2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00
Albert Vaca Cintora ae23413971 Refactor BackgroundService
Added a new KdeConnect Application class that holds the Devices now, while
BackgroundService "only" takes care of the LinkProviders.

Since KdeConnect subclasses Application we have the guarantee that it will
exist as long as our process does, so we can use it as a singleton. This
removes the "BackgroundService.RunCommand" hack (which sent an Intent that
would awake BackgroundService in case it wasn't running already and then
call our code in a callback). This saves lots of round trips between the
system and us and makes things simpler (and stack traces useful) by making
the code sequential.

We already had an Application subclass that I moved to a new helper, which
now the KdeConnect class initializes together with all the other helpers.
2023-05-24 19:28:02 +02:00

585 lines
19 KiB
Java

/*
* SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.MousePadPlugin;
import android.content.Intent;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
import org.kde.kdeconnect_tp.R;
import java.util.Objects;
public class MousePadActivity
extends AppCompatActivity
implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener,
MousePadGestureDetector.OnGestureListener,
SensorEventListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private String deviceId;
private final static float MinDistanceToSendScroll = 2.5f; // touch gesture scroll
private final static float MinDistanceToSendGenericScroll = 0.1f; // real mouse scroll wheel event
private final static float StandardDpi = 240.0f; // = hdpi
private float mPrevX;
private float mPrevY;
private float mCurrentX;
private float mCurrentY;
private float mCurrentSensitivity;
private float displayDpiMultiplier;
private int scrollDirection = 1;
private boolean allowGyro = false;
private boolean gyroEnabled = false;
private int gyroscopeSensitivity = 100;
private boolean isScrolling = false;
private float accumulatedDistanceY = 0;
private GestureDetector mDetector;
private SensorManager mSensorManager;
private MousePadGestureDetector mMousePadGestureDetector;
private PointerAccelerationProfile mPointerAccelerationProfile;
private PointerAccelerationProfile.MouseDelta mouseDelta; // to be reused on every touch move event
private KeyListenerView keyListenerView;
private SharedPreferences prefs = null;
private boolean prefsApplied = false;
enum ClickType {
LEFT, RIGHT, MIDDLE, NONE;
static ClickType fromString(String s) {
switch (s) {
case "left":
return LEFT;
case "right":
return RIGHT;
case "middle":
return MIDDLE;
default:
return NONE;
}
}
}
private ClickType singleTapAction, doubleTapAction, tripleTapAction;
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
@Override
public void onSensorChanged(SensorEvent event) {
float[] values = event.values;
float X = -values[2] * 70 * (gyroscopeSensitivity/100.0f);
float Y = -values[0] * 70 * (gyroscopeSensitivity/100.0f);
if (X < 0.25 && X > -0.25) {
X = 0;
} else {
X = X * (gyroscopeSensitivity/100.0f);
}
if (Y < 0.25 && Y > -0.25) {
Y = 0;
} else {
Y = Y * (gyroscopeSensitivity/100.0f);
}
final float nX = X;
final float nY = Y;
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendMouseDelta(nX, nY);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mousepad);
setSupportActionBar(findViewById(R.id.toolbar));
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
findViewById(R.id.mouse_click_left).setOnClickListener(v -> sendLeftClick());
findViewById(R.id.mouse_click_middle).setOnClickListener(v -> sendMiddleClick());
findViewById(R.id.mouse_click_right).setOnClickListener(v -> sendRightClick());
deviceId = getIntent().getStringExtra("deviceId");
getWindow().getDecorView().setHapticFeedbackEnabled(true);
mDetector = new GestureDetector(this, this);
mMousePadGestureDetector = new MousePadGestureDetector(this);
mDetector.setOnDoubleTapListener(this);
mSensorManager = ContextCompat.getSystemService(this, SensorManager.class);
keyListenerView = findViewById(R.id.keyListener);
keyListenerView.setDeviceId(deviceId);
prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(this);
applyPrefs();
//Technically xdpi and ydpi should be handled separately,
//but since ydpi is usually almost equal to xdpi, only xdpi is used for the multiplier.
displayDpiMultiplier = StandardDpi / getResources().getDisplayMetrics().xdpi;
final View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(visibility -> {
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
int fullscreenType = 0;
fullscreenType |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
fullscreenType |= View.SYSTEM_UI_FLAG_FULLSCREEN;
fullscreenType |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
getWindow().getDecorView().setSystemUiVisibility(fullscreenType);
}
});
}
@Override
protected void onResume() {
applyPrefs();
if (allowGyro && !gyroEnabled) {
mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME);
gyroEnabled = true;
}
invalidateMenu();
super.onResume();
}
@Override
protected void onPause() {
if (gyroEnabled) {
mSensorManager.unregisterListener(this);
gyroEnabled = false;
}
super.onPause();
}
@Override
protected void onStop() {
if (gyroEnabled) {
mSensorManager.unregisterListener(this);
gyroEnabled = false;
}
super.onStop();
}
@Override
protected void onDestroy() {
prefs.unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_mousepad, menu);
boolean mouseButtonsEnabled = prefs
.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true);
menu.findItem(R.id.menu_right_click).setVisible(!mouseButtonsEnabled);
menu.findItem(R.id.menu_middle_click).setVisible(!mouseButtonsEnabled);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_right_click) {
sendRightClick();
return true;
} else if (id == R.id.menu_middle_click) {
sendMiddleClick();
return true;
} else if (id == R.id.menu_open_mousepad_settings) {
Intent intent = new Intent(this, PluginSettingsActivity.class)
.putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, deviceId)
.putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, MousePadPlugin.class.getSimpleName());
startActivity(intent);
return true;
} else if (id == R.id.menu_show_keyboard) {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return true;
}
if (plugin.isKeyboardEnabled()) {
showKeyboard();
} else {
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
toast.show();
}
return true;
} else if (id == R.id.menu_open_compose_send) {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return true;
}
if (plugin.isKeyboardEnabled()) {
showCompose();
} else {
Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT);
toast.show();
}
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mMousePadGestureDetector.onTouchEvent(event)) {
return true;
}
if (mDetector.onTouchEvent(event)) {
return true;
}
int actionType = event.getAction();
if (isScrolling) {
if (actionType == MotionEvent.ACTION_UP) {
isScrolling = false;
} else {
return false;
}
}
switch (actionType) {
case MotionEvent.ACTION_DOWN:
mPrevX = event.getX();
mPrevY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
mCurrentX = event.getX();
mCurrentY = event.getY();
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return true;
}
float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity;
float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity;
// Run the mouse delta through the pointer acceleration profile
mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime());
mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta);
plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y);
mPrevX = mCurrentX;
mPrevY = mCurrentY;
break;
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//From GestureDetector, left empty
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onGenericMotionEvent(MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_SCROLL) {
final float distanceY = e.getAxisValue(MotionEvent.AXIS_VSCROLL);
accumulatedDistanceY += distanceY;
if (accumulatedDistanceY > MinDistanceToSendGenericScroll || accumulatedDistanceY < -MinDistanceToSendGenericScroll) {
sendScroll(accumulatedDistanceY);
accumulatedDistanceY = 0;
}
}
return super.onGenericMotionEvent(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, final float distanceX, final float distanceY) {
// If only one thumb is used then cancel the scroll gesture
if (e2.getPointerCount() <= 1) {
return false;
}
isScrolling = true;
accumulatedDistanceY += distanceY;
if (accumulatedDistanceY > MinDistanceToSendScroll || accumulatedDistanceY < -MinDistanceToSendScroll) {
sendScroll(scrollDirection * accumulatedDistanceY);
accumulatedDistanceY = 0;
}
return true;
}
@Override
public void onLongPress(MotionEvent e) {
getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendSingleHold();
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
switch (singleTapAction) {
case LEFT:
sendLeftClick();
break;
case RIGHT:
sendRightClick();
break;
case MIDDLE:
sendMiddleClick();
break;
default:
}
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return true;
}
plugin.sendDoubleClick();
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
@Override
public boolean onTripleFingerTap(MotionEvent ev) {
switch (tripleTapAction) {
case LEFT:
sendLeftClick();
break;
case RIGHT:
sendRightClick();
break;
case MIDDLE:
sendMiddleClick();
break;
default:
}
return true;
}
@Override
public boolean onDoubleFingerTap(MotionEvent ev) {
switch (doubleTapAction) {
case LEFT:
sendLeftClick();
break;
case RIGHT:
sendRightClick();
break;
case MIDDLE:
sendMiddleClick();
break;
default:
}
return true;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (prefsApplied) prefsApplied = false;
}
private void sendLeftClick() {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendLeftClick();
}
private void sendMiddleClick() {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendMiddleClick();
}
private void sendRightClick() {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendRightClick();
}
private void sendScroll(final float y) {
MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class);
if (plugin == null) {
finish();
return;
}
plugin.sendScroll(0, y);
}
private void showKeyboard() {
InputMethodManager imm = ContextCompat.getSystemService(this, InputMethodManager.class);
keyListenerView.requestFocus();
imm.toggleSoftInputFromWindow(keyListenerView.getWindowToken(), 0, 0);
}
private void showCompose() {
Intent intent = new Intent(this, ComposeSendActivity.class);
intent.putExtra("org.kde.kdeconnect.Plugins.MousePadPlugin.deviceId", deviceId);
startActivity(intent);
}
private void applyPrefs() {
if (prefsApplied) return;
if (prefs.getBoolean(getString(R.string.mousepad_scroll_direction), false)) {
scrollDirection = -1;
} else {
scrollDirection = 1;
}
allowGyro = isGyroSensorAvailable() && prefs.getBoolean(getString(R.string.gyro_mouse_enabled), false);
if (allowGyro) gyroscopeSensitivity = prefs.getInt(getString(R.string.gyro_mouse_sensitivity), 100);
String singleTapSetting = prefs.getString(getString(R.string.mousepad_single_tap_key),
getString(R.string.mousepad_default_single));
String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key),
getString(R.string.mousepad_default_double));
String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key),
getString(R.string.mousepad_default_triple));
String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key),
getString(R.string.mousepad_default_sensitivity));
String accelerationProfileName = prefs.getString(getString(R.string.mousepad_acceleration_profile_key),
getString(R.string.mousepad_default_acceleration_profile));
mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName);
singleTapAction = ClickType.fromString(singleTapSetting);
doubleTapAction = ClickType.fromString(doubleTapSetting);
tripleTapAction = ClickType.fromString(tripleTapSetting);
switch (sensitivitySetting) {
case "slowest":
mCurrentSensitivity = 0.2f;
break;
case "aboveSlowest":
mCurrentSensitivity = 0.5f;
break;
case "default":
mCurrentSensitivity = 1.0f;
break;
case "aboveDefault":
mCurrentSensitivity = 1.5f;
break;
case "fastest":
mCurrentSensitivity = 2.0f;
break;
default:
mCurrentSensitivity = 1.0f;
return;
}
if (prefs.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true)) {
findViewById(R.id.mouse_buttons).setVisibility(View.VISIBLE);
} else {
findViewById(R.id.mouse_buttons).setVisibility(View.GONE);
}
prefsApplied = true;
}
private boolean isGyroSensorAvailable() {
return mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
}
@Override
public boolean onSupportNavigateUp() {
super.onBackPressed();
return true;
}
}