File indexing completed on 2024-12-22 04:41:39
0001 /* 0002 * SPDX-FileCopyrightText: 2014 Ahmed I. Khalil <ahmedibrahimkhali@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 package org.kde.kdeconnect.Plugins.MousePadPlugin; 0008 0009 import android.content.Intent; 0010 import android.content.SharedPreferences; 0011 import android.hardware.Sensor; 0012 import android.hardware.SensorEvent; 0013 import android.hardware.SensorEventListener; 0014 import android.hardware.SensorManager; 0015 import android.os.Bundle; 0016 import android.view.GestureDetector; 0017 import android.view.HapticFeedbackConstants; 0018 import android.view.Menu; 0019 import android.view.MenuInflater; 0020 import android.view.MenuItem; 0021 import android.view.MotionEvent; 0022 import android.view.View; 0023 import android.view.inputmethod.InputMethodManager; 0024 import android.widget.Toast; 0025 0026 import androidx.appcompat.app.AppCompatActivity; 0027 import androidx.core.content.ContextCompat; 0028 import androidx.preference.PreferenceManager; 0029 0030 import org.kde.kdeconnect.KdeConnect; 0031 import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; 0032 import org.kde.kdeconnect_tp.R; 0033 0034 import java.util.Objects; 0035 0036 public class MousePadActivity 0037 extends AppCompatActivity 0038 implements GestureDetector.OnGestureListener, 0039 GestureDetector.OnDoubleTapListener, 0040 MousePadGestureDetector.OnGestureListener, 0041 SensorEventListener, 0042 SharedPreferences.OnSharedPreferenceChangeListener { 0043 private String deviceId; 0044 0045 private final static float MinDistanceToSendScroll = 2.5f; // touch gesture scroll 0046 private final static float MinDistanceToSendGenericScroll = 0.1f; // real mouse scroll wheel event 0047 private final static float StandardDpi = 240.0f; // = hdpi 0048 0049 private float mPrevX; 0050 private float mPrevY; 0051 private float mCurrentX; 0052 private float mCurrentY; 0053 private float mCurrentSensitivity; 0054 private float displayDpiMultiplier; 0055 private int scrollDirection = 1; 0056 private double scrollCoefficient = 1.0; 0057 private boolean allowGyro = false; 0058 private boolean gyroEnabled = false; 0059 private int gyroscopeSensitivity = 100; 0060 private boolean isScrolling = false; 0061 private float accumulatedDistanceY = 0; 0062 0063 private GestureDetector mDetector; 0064 private SensorManager mSensorManager; 0065 private MousePadGestureDetector mMousePadGestureDetector; 0066 private PointerAccelerationProfile mPointerAccelerationProfile; 0067 0068 private PointerAccelerationProfile.MouseDelta mouseDelta; // to be reused on every touch move event 0069 0070 private KeyListenerView keyListenerView; 0071 0072 private SharedPreferences prefs = null; 0073 0074 private boolean prefsApplied = false; 0075 0076 enum ClickType { 0077 LEFT, RIGHT, MIDDLE, NONE; 0078 0079 static ClickType fromString(String s) { 0080 switch (s) { 0081 case "left": 0082 return LEFT; 0083 case "right": 0084 return RIGHT; 0085 case "middle": 0086 return MIDDLE; 0087 default: 0088 return NONE; 0089 } 0090 } 0091 } 0092 0093 private ClickType singleTapAction, doubleTapAction, tripleTapAction; 0094 0095 @Override 0096 public void onAccuracyChanged(Sensor sensor, int accuracy) { 0097 } 0098 0099 @Override 0100 public void onSensorChanged(SensorEvent event) { 0101 float[] values = event.values; 0102 0103 float X = -values[2] * 70 * (gyroscopeSensitivity/100.0f); 0104 float Y = -values[0] * 70 * (gyroscopeSensitivity/100.0f); 0105 0106 if (X < 0.25 && X > -0.25) { 0107 X = 0; 0108 } else { 0109 X = X * (gyroscopeSensitivity/100.0f); 0110 } 0111 0112 if (Y < 0.25 && Y > -0.25) { 0113 Y = 0; 0114 } else { 0115 Y = Y * (gyroscopeSensitivity/100.0f); 0116 } 0117 0118 final float nX = X; 0119 final float nY = Y; 0120 0121 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0122 if (plugin == null) { 0123 finish(); 0124 return; 0125 } 0126 plugin.sendMouseDelta(nX, nY); 0127 } 0128 0129 @Override 0130 protected void onCreate(Bundle savedInstanceState) { 0131 super.onCreate(savedInstanceState); 0132 0133 setContentView(R.layout.activity_mousepad); 0134 0135 setSupportActionBar(findViewById(R.id.toolbar)); 0136 Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); 0137 getSupportActionBar().setDisplayShowHomeEnabled(true); 0138 0139 findViewById(R.id.mouse_click_left).setOnClickListener(v -> sendLeftClick()); 0140 findViewById(R.id.mouse_click_middle).setOnClickListener(v -> sendMiddleClick()); 0141 findViewById(R.id.mouse_click_right).setOnClickListener(v -> sendRightClick()); 0142 0143 deviceId = getIntent().getStringExtra("deviceId"); 0144 0145 getWindow().getDecorView().setHapticFeedbackEnabled(true); 0146 0147 mDetector = new GestureDetector(this, this); 0148 mMousePadGestureDetector = new MousePadGestureDetector(this); 0149 mDetector.setOnDoubleTapListener(this); 0150 mSensorManager = ContextCompat.getSystemService(this, SensorManager.class); 0151 0152 keyListenerView = findViewById(R.id.keyListener); 0153 keyListenerView.setDeviceId(deviceId); 0154 0155 prefs = PreferenceManager.getDefaultSharedPreferences(this); 0156 prefs.registerOnSharedPreferenceChangeListener(this); 0157 0158 applyPrefs(); 0159 0160 //Technically xdpi and ydpi should be handled separately, 0161 //but since ydpi is usually almost equal to xdpi, only xdpi is used for the multiplier. 0162 displayDpiMultiplier = StandardDpi / getResources().getDisplayMetrics().xdpi; 0163 0164 final View decorView = getWindow().getDecorView(); 0165 decorView.setOnSystemUiVisibilityChangeListener(visibility -> { 0166 if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { 0167 0168 int fullscreenType = 0; 0169 0170 fullscreenType |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; 0171 fullscreenType |= View.SYSTEM_UI_FLAG_FULLSCREEN; 0172 fullscreenType |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 0173 0174 getWindow().getDecorView().setSystemUiVisibility(fullscreenType); 0175 } 0176 }); 0177 } 0178 0179 @Override 0180 protected void onResume() { 0181 applyPrefs(); 0182 0183 if (allowGyro && !gyroEnabled) { 0184 mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), SensorManager.SENSOR_DELAY_GAME); 0185 gyroEnabled = true; 0186 } 0187 0188 invalidateMenu(); 0189 0190 super.onResume(); 0191 } 0192 0193 @Override 0194 protected void onPause() { 0195 if (gyroEnabled) { 0196 mSensorManager.unregisterListener(this); 0197 gyroEnabled = false; 0198 } 0199 super.onPause(); 0200 } 0201 0202 @Override 0203 protected void onStop() { 0204 if (gyroEnabled) { 0205 mSensorManager.unregisterListener(this); 0206 gyroEnabled = false; 0207 } 0208 super.onStop(); 0209 } 0210 0211 @Override 0212 protected void onDestroy() { 0213 prefs.unregisterOnSharedPreferenceChangeListener(this); 0214 super.onDestroy(); 0215 } 0216 0217 @Override 0218 public boolean onCreateOptionsMenu(Menu menu) { 0219 MenuInflater inflater = getMenuInflater(); 0220 inflater.inflate(R.menu.menu_mousepad, menu); 0221 0222 boolean mouseButtonsEnabled = prefs 0223 .getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true); 0224 menu.findItem(R.id.menu_right_click).setVisible(!mouseButtonsEnabled); 0225 menu.findItem(R.id.menu_middle_click).setVisible(!mouseButtonsEnabled); 0226 0227 return true; 0228 } 0229 0230 @Override 0231 public boolean onOptionsItemSelected(MenuItem item) { 0232 int id = item.getItemId(); 0233 if (id == R.id.menu_right_click) { 0234 sendRightClick(); 0235 return true; 0236 } else if (id == R.id.menu_middle_click) { 0237 sendMiddleClick(); 0238 return true; 0239 } else if (id == R.id.menu_open_mousepad_settings) { 0240 Intent intent = new Intent(this, PluginSettingsActivity.class) 0241 .putExtra(PluginSettingsActivity.EXTRA_DEVICE_ID, deviceId) 0242 .putExtra(PluginSettingsActivity.EXTRA_PLUGIN_KEY, MousePadPlugin.class.getSimpleName()); 0243 startActivity(intent); 0244 return true; 0245 } else if (id == R.id.menu_show_keyboard) { 0246 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0247 if (plugin == null) { 0248 finish(); 0249 return true; 0250 } 0251 if (plugin.isKeyboardEnabled()) { 0252 showKeyboard(); 0253 } else { 0254 Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT); 0255 toast.show(); 0256 } 0257 return true; 0258 } else if (id == R.id.menu_open_compose_send) { 0259 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0260 if (plugin == null) { 0261 finish(); 0262 return true; 0263 } 0264 if (plugin.isKeyboardEnabled()) { 0265 showCompose(); 0266 } else { 0267 Toast toast = Toast.makeText(this, R.string.mousepad_keyboard_input_not_supported, Toast.LENGTH_SHORT); 0268 toast.show(); 0269 } 0270 return true; 0271 } else { 0272 return super.onOptionsItemSelected(item); 0273 } 0274 } 0275 0276 @Override 0277 public boolean onTouchEvent(MotionEvent event) { 0278 if (mMousePadGestureDetector.onTouchEvent(event)) { 0279 return true; 0280 } 0281 if (mDetector.onTouchEvent(event)) { 0282 return true; 0283 } 0284 0285 int actionType = event.getAction(); 0286 0287 if (isScrolling) { 0288 if (actionType == MotionEvent.ACTION_UP) { 0289 isScrolling = false; 0290 } else { 0291 return false; 0292 0293 } 0294 } 0295 0296 switch (actionType) { 0297 case MotionEvent.ACTION_DOWN: 0298 mPrevX = event.getX(); 0299 mPrevY = event.getY(); 0300 break; 0301 case MotionEvent.ACTION_MOVE: 0302 mCurrentX = event.getX(); 0303 mCurrentY = event.getY(); 0304 0305 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0306 if (plugin == null) { 0307 finish(); 0308 return true; 0309 } 0310 0311 float deltaX = (mCurrentX - mPrevX) * displayDpiMultiplier * mCurrentSensitivity; 0312 float deltaY = (mCurrentY - mPrevY) * displayDpiMultiplier * mCurrentSensitivity; 0313 0314 // Run the mouse delta through the pointer acceleration profile 0315 mPointerAccelerationProfile.touchMoved(deltaX, deltaY, event.getEventTime()); 0316 mouseDelta = mPointerAccelerationProfile.commitAcceleratedMouseDelta(mouseDelta); 0317 0318 plugin.sendMouseDelta(mouseDelta.x, mouseDelta.y); 0319 0320 mPrevX = mCurrentX; 0321 mPrevY = mCurrentY; 0322 0323 break; 0324 } 0325 return true; 0326 } 0327 0328 @Override 0329 public boolean onDown(MotionEvent e) { 0330 return false; 0331 } 0332 0333 @Override 0334 public void onShowPress(MotionEvent e) { 0335 //From GestureDetector, left empty 0336 } 0337 0338 @Override 0339 public boolean onSingleTapUp(MotionEvent e) { 0340 return false; 0341 } 0342 0343 @Override 0344 public boolean onGenericMotionEvent(MotionEvent e) { 0345 if (e.getAction() == MotionEvent.ACTION_SCROLL) { 0346 final float distanceY = e.getAxisValue(MotionEvent.AXIS_VSCROLL); 0347 0348 accumulatedDistanceY += distanceY; 0349 0350 if (accumulatedDistanceY > MinDistanceToSendGenericScroll || accumulatedDistanceY < -MinDistanceToSendGenericScroll) { 0351 sendScroll(accumulatedDistanceY); 0352 accumulatedDistanceY = 0; 0353 } 0354 } 0355 0356 return super.onGenericMotionEvent(e); 0357 } 0358 0359 @Override 0360 public boolean onScroll(MotionEvent e1, MotionEvent e2, final float distanceX, final float distanceY) { 0361 // If only one thumb is used then cancel the scroll gesture 0362 if (e2.getPointerCount() <= 1) { 0363 return false; 0364 } 0365 0366 isScrolling = true; 0367 0368 accumulatedDistanceY += distanceY * scrollCoefficient; 0369 if (accumulatedDistanceY > MinDistanceToSendScroll || accumulatedDistanceY < -MinDistanceToSendScroll) { 0370 sendScroll(scrollDirection * accumulatedDistanceY); 0371 0372 accumulatedDistanceY = 0; 0373 } 0374 0375 return true; 0376 } 0377 0378 @Override 0379 public void onLongPress(MotionEvent e) { 0380 getWindow().getDecorView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 0381 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0382 if (plugin == null) { 0383 finish(); 0384 return; 0385 } 0386 plugin.sendSingleHold(); 0387 } 0388 0389 @Override 0390 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 0391 return false; 0392 } 0393 0394 @Override 0395 public boolean onSingleTapConfirmed(MotionEvent e) { 0396 switch (singleTapAction) { 0397 case LEFT: 0398 sendLeftClick(); 0399 break; 0400 case RIGHT: 0401 sendRightClick(); 0402 break; 0403 case MIDDLE: 0404 sendMiddleClick(); 0405 break; 0406 default: 0407 } 0408 return true; 0409 } 0410 0411 @Override 0412 public boolean onDoubleTap(MotionEvent e) { 0413 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0414 if (plugin == null) { 0415 finish(); 0416 return true; 0417 } 0418 plugin.sendDoubleClick(); 0419 return true; 0420 } 0421 0422 @Override 0423 public boolean onDoubleTapEvent(MotionEvent e) { 0424 return false; 0425 } 0426 0427 @Override 0428 public boolean onTripleFingerTap(MotionEvent ev) { 0429 switch (tripleTapAction) { 0430 case LEFT: 0431 sendLeftClick(); 0432 break; 0433 case RIGHT: 0434 sendRightClick(); 0435 break; 0436 case MIDDLE: 0437 sendMiddleClick(); 0438 break; 0439 default: 0440 } 0441 return true; 0442 } 0443 0444 @Override 0445 public boolean onDoubleFingerTap(MotionEvent ev) { 0446 switch (doubleTapAction) { 0447 case LEFT: 0448 sendLeftClick(); 0449 break; 0450 case RIGHT: 0451 sendRightClick(); 0452 break; 0453 case MIDDLE: 0454 sendMiddleClick(); 0455 break; 0456 default: 0457 } 0458 return true; 0459 } 0460 0461 @Override 0462 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 0463 if (prefsApplied) prefsApplied = false; 0464 } 0465 0466 0467 private void sendLeftClick() { 0468 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0469 if (plugin == null) { 0470 finish(); 0471 return; 0472 } 0473 plugin.sendLeftClick(); 0474 } 0475 0476 private void sendMiddleClick() { 0477 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0478 if (plugin == null) { 0479 finish(); 0480 return; 0481 } 0482 plugin.sendMiddleClick(); 0483 } 0484 0485 private void sendRightClick() { 0486 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0487 if (plugin == null) { 0488 finish(); 0489 return; 0490 } 0491 plugin.sendRightClick(); 0492 } 0493 0494 private void sendScroll(final float y) { 0495 MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MousePadPlugin.class); 0496 if (plugin == null) { 0497 finish(); 0498 return; 0499 } 0500 plugin.sendScroll(0, y); 0501 } 0502 0503 private void showKeyboard() { 0504 InputMethodManager imm = ContextCompat.getSystemService(this, InputMethodManager.class); 0505 keyListenerView.requestFocus(); 0506 imm.toggleSoftInputFromWindow(keyListenerView.getWindowToken(), 0, 0); 0507 } 0508 0509 private void showCompose() { 0510 Intent intent = new Intent(this, ComposeSendActivity.class); 0511 intent.putExtra("org.kde.kdeconnect.Plugins.MousePadPlugin.deviceId", deviceId); 0512 startActivity(intent); 0513 } 0514 0515 private void applyPrefs() { 0516 if (prefsApplied) return; 0517 0518 if (prefs.getBoolean(getString(R.string.mousepad_scroll_direction), false)) { 0519 scrollDirection = -1; 0520 } else { 0521 scrollDirection = 1; 0522 } 0523 0524 int scrollSensitivity = prefs.getInt(getString(R.string.mousepad_scroll_sensitivity), 100); 0525 if (scrollSensitivity == 0) scrollSensitivity = 1; 0526 scrollCoefficient = Math.pow((scrollSensitivity / 100f), 1.5); 0527 0528 allowGyro = isGyroSensorAvailable() && prefs.getBoolean(getString(R.string.gyro_mouse_enabled), false); 0529 if (allowGyro) gyroscopeSensitivity = prefs.getInt(getString(R.string.gyro_mouse_sensitivity), 100); 0530 0531 String singleTapSetting = prefs.getString(getString(R.string.mousepad_single_tap_key), 0532 getString(R.string.mousepad_default_single)); 0533 String doubleTapSetting = prefs.getString(getString(R.string.mousepad_double_tap_key), 0534 getString(R.string.mousepad_default_double)); 0535 String tripleTapSetting = prefs.getString(getString(R.string.mousepad_triple_tap_key), 0536 getString(R.string.mousepad_default_triple)); 0537 String sensitivitySetting = prefs.getString(getString(R.string.mousepad_sensitivity_key), 0538 getString(R.string.mousepad_default_sensitivity)); 0539 0540 String accelerationProfileName = prefs.getString(getString(R.string.mousepad_acceleration_profile_key), 0541 getString(R.string.mousepad_default_acceleration_profile)); 0542 0543 mPointerAccelerationProfile = PointerAccelerationProfileFactory.getProfileWithName(accelerationProfileName); 0544 0545 singleTapAction = ClickType.fromString(singleTapSetting); 0546 doubleTapAction = ClickType.fromString(doubleTapSetting); 0547 tripleTapAction = ClickType.fromString(tripleTapSetting); 0548 0549 switch (sensitivitySetting) { 0550 case "slowest": 0551 mCurrentSensitivity = 0.2f; 0552 break; 0553 case "aboveSlowest": 0554 mCurrentSensitivity = 0.5f; 0555 break; 0556 case "default": 0557 mCurrentSensitivity = 1.0f; 0558 break; 0559 case "aboveDefault": 0560 mCurrentSensitivity = 1.5f; 0561 break; 0562 case "fastest": 0563 mCurrentSensitivity = 2.0f; 0564 break; 0565 default: 0566 mCurrentSensitivity = 1.0f; 0567 return; 0568 } 0569 0570 if (prefs.getBoolean(getString(R.string.mousepad_mouse_buttons_enabled_pref), true)) { 0571 findViewById(R.id.mouse_buttons).setVisibility(View.VISIBLE); 0572 } else { 0573 findViewById(R.id.mouse_buttons).setVisibility(View.GONE); 0574 } 0575 0576 prefsApplied = true; 0577 } 0578 0579 private boolean isGyroSensorAvailable() { 0580 return mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null; 0581 } 0582 0583 @Override 0584 public boolean onSupportNavigateUp() { 0585 super.onBackPressed(); 0586 return true; 0587 } 0588 } 0589