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