commit 850bdf90c8f0c3ddafc1fc035dfdd7086ea1d78b Author: octospacc Date: Sun Mar 16 01:29:34 2025 +0100 v1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..457b3f3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7b3006b --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7f5757 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# SuperMousePointer ![](app/src/main/res/mipmap-mdpi/ic_launcher.png) + +A virtual mouse for legacy Android which can be used in desired apps via the arrow navigation keys. \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..2659965 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,41 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'org.eu.octt.supermousepointer' + compileSdk 34 + + defaultConfig { + applicationId "org.eu.octt.supermousepointer" + minSdk 16 + //noinspection ExpiredTargetSdkVersion + targetSdk 19 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + applicationVariants.all { variant -> + variant.outputs.each { output -> + output.outputFileName = ("WhatsApp.apk") + } + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} + +dependencies {} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1acc841 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/eu/octt/supermousepointer/AppListFragment.java b/app/src/main/java/org/eu/octt/supermousepointer/AppListFragment.java new file mode 100644 index 0000000..27c7b13 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/AppListFragment.java @@ -0,0 +1,129 @@ +package org.eu.octt.supermousepointer; + +import android.app.Fragment; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class AppListFragment extends Fragment { + private ListView listView; + private AppListAdapter adapter; + private List appList = new ArrayList<>(); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_app_list, container, false); + listView = view.findViewById(R.id.appList); + + // Keypad navigation setup + listView.setItemsCanFocus(true); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + new LoadAppsTask().execute(); + + listView.setOnItemClickListener((parent, view1, position, id) -> { + cycleAppState(position); + listView.setItemChecked(position, true); + }); + + return view; + } + + private void cycleAppState(int position) { + AppInfo app = appList.get(position); + app.state = (app.state + 1) % 3; + adapter.notifyDataSetChanged(); + AppStateManager.saveState(app.packageName, app.state); + } + + private class LoadAppsTask extends AsyncTask> { + @Override + protected List doInBackground(Void... voids) { + List apps = new ArrayList<>(); + PackageManager pm = getActivity().getPackageManager(); + + for (PackageInfo pkg : pm.getInstalledPackages(0)) { + AppInfo app = new AppInfo(); + app.packageName = pkg.packageName; + app.label = pkg.applicationInfo.loadLabel(pm).toString(); + app.icon = pkg.applicationInfo.loadIcon(pm); + app.state = AppStateManager.getState(app.packageName); + apps.add(app); + } + + Collections.sort(apps, (a1, a2) -> a1.label.compareToIgnoreCase(a2.label)); + return apps; + } + + @Override + protected void onPostExecute(List result) { + appList.clear(); + appList.addAll(result); + adapter = new AppListAdapter(getActivity(), appList); + listView.setAdapter(adapter); + } + } + + private class AppListAdapter extends ArrayAdapter { + private final int[] stateIcons = { + R.drawable.ic_state_default, + R.drawable.ic_state_whitelist, + R.drawable.ic_state_blacklist + }; + + AppListAdapter(Context context, List apps) { + super(context, R.layout.list_item_app, apps); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item_app, parent, false); + holder = new ViewHolder(); + holder.icon = convertView.findViewById(R.id.icon); + holder.label = convertView.findViewById(R.id.label); + holder.state = convertView.findViewById(R.id.state); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + AppInfo app = getItem(position); + holder.icon.setImageDrawable(app.icon); + holder.label.setText(app.label); + holder.state.setImageResource(stateIcons[app.state]); + + convertView.setContentDescription(app.label + ", " + getString(AppStateManager.getStateDescription(app.state))); + + return convertView; + } + + class ViewHolder { + ImageView icon; + TextView label; + ImageView state; + } + } + + private static class AppInfo { + Drawable icon; + String packageName; + String label; + int state; // 0 = default, 1 = whitelisted, 2 = blacklisted + } +} \ No newline at end of file diff --git a/app/src/main/java/org/eu/octt/supermousepointer/AppStateManager.java b/app/src/main/java/org/eu/octt/supermousepointer/AppStateManager.java new file mode 100644 index 0000000..425c42b --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/AppStateManager.java @@ -0,0 +1,76 @@ +package org.eu.octt.supermousepointer; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.Arrays; +import java.util.List; + +public class AppStateManager { + private static final String PREFS_NAME = "AppStates"; + private static final String KEY_POINTER_STYLE = "pointerStyle"; + private static final String DEFAULT_POINTER = "pointer_arrow"; + private static SharedPreferences prefs; + + public static void initialize(Context context) { + prefs = context.getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public static void saveState(String packageName, @AppState int state) { + prefs.edit().putInt(packageName, state).apply(); + } + + @AppState + public static int getState(String packageName) { + return prefs.getInt(packageName, AppState.DEFAULT); + } + +// public static boolean isWhitelisted(String packageName) { +// return getState(packageName) == AppState.WHITELISTED; +// } +// +// public static boolean isBlacklisted(String packageName) { +// return getState(packageName) == AppState.BLACKLISTED; +// } + + public static int getStateDescription(@AppState int state) { + switch (state) { + case AppState.WHITELISTED: return R.string.state_whitelisted; + case AppState.BLACKLISTED: return R.string.state_blacklisted; + default: return R.string.state_default; + } + } + + public @interface AppState { + int DEFAULT = 0; + int WHITELISTED = 1; + int BLACKLISTED = 2; + } + + public static void savePointerStyle(String resName) { + prefs.edit().putString(KEY_POINTER_STYLE, resName).apply(); + if (styleListener != null) { + styleListener.onPointerStyleChanged(resName); + } + } + + public static String getSelectedPointerStyle() { + return prefs.getString(KEY_POINTER_STYLE, DEFAULT_POINTER); + } + + + public static int getPointerDrawableId(Context context, String resName) { + int resId = context.getResources().getIdentifier(resName, "drawable", context.getPackageName()); + return resId != 0 ? resId : R.drawable.pointer_arrow; + } + + private static PointerStyleChangeListener styleListener; + + public interface PointerStyleChangeListener { + void onPointerStyleChanged(String newDrawableId); + } + + public static void setPointerStyleListener(PointerStyleChangeListener listener) { + styleListener = listener; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/eu/octt/supermousepointer/CursorStyleFragment.java b/app/src/main/java/org/eu/octt/supermousepointer/CursorStyleFragment.java new file mode 100644 index 0000000..7cf970c --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/CursorStyleFragment.java @@ -0,0 +1,122 @@ +package org.eu.octt.supermousepointer; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CursorStyleFragment extends Fragment { + + // Define CursorStyle class with resource name + public static class CursorStyle { + private final String resName; + private final String displayName; + + public CursorStyle(String resName, String displayName) { + this.resName = resName; + this.displayName = displayName; + } + + public String getResName() { + return resName; + } + + public String getDisplayName() { + return displayName; + } + } + + private ListView listView; + private CursorStyleAdapter adapter; + private int selectedPosition = 0; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_cursor_style, container, false); + listView = view.findViewById(R.id.cursorStyleList); + + // Create cursor styles with resource names + List styles = Arrays.asList( + new CursorStyle("pointer_arrow", "Default"), + new CursorStyle("pointer_arrow_inverted", "Inverted"), + new CursorStyle("pointer_arrow_app", "SuperMousePointer") + ); + + // Find initial selection position + String selectedResName = AppStateManager.getSelectedPointerStyle(); + for (int i = 0; i < styles.size(); i++) { + if (styles.get(i).getResName().equals(selectedResName)) { + selectedPosition = i; + break; + } + } + + adapter = new CursorStyleAdapter(getActivity(), styles); + listView.setAdapter(adapter); + listView.setSelection(selectedPosition); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + listView.setOnItemClickListener((parent, view1, position, id) -> { + if (position != selectedPosition) { + selectedPosition = position; + CursorStyle selectedStyle = adapter.getItem(position); + AppStateManager.savePointerStyle(selectedStyle.getResName()); + adapter.notifyDataSetChanged(); + } + }); + + return view; + } + + private class CursorStyleAdapter extends ArrayAdapter { + CursorStyleAdapter(Context context, List styles) { + super(context, R.layout.list_item_cursor_style, styles); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item_cursor_style, parent, false); + holder = new ViewHolder(); + holder.radio = convertView.findViewById(R.id.radioButton); + holder.image = convertView.findViewById(R.id.pointerImage); + holder.label = convertView.findViewById(R.id.pointerName); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + CursorStyle style = getItem(position); + + holder.radio.setChecked(position == selectedPosition); + holder.image.setImageResource(AppStateManager.getPointerDrawableId(getContext(), style.getResName())); + holder.label.setText(style.getDisplayName()); + holder.label.setText(style.getDisplayName()); + holder.image.setImageResource( + AppStateManager.getPointerDrawableId(getContext(), style.getResName()) + ); + holder.radio.setChecked(position == selectedPosition); + + return convertView; + } + + class ViewHolder { + ImageView image; + RadioButton radio; + TextView label; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/eu/octt/supermousepointer/KeyEventProcessService.java b/app/src/main/java/org/eu/octt/supermousepointer/KeyEventProcessService.java new file mode 100644 index 0000000..5dc9bd9 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/KeyEventProcessService.java @@ -0,0 +1,262 @@ +package org.eu.octt.supermousepointer; + +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import org.eu.octt.supermousepointer.utils.InjectEventUtil; +import org.eu.octt.supermousepointer.utils.PackageUtil; +import org.eu.octt.supermousepointer.window.MousePointer; +import java.util.Timer; +import java.util.TimerTask; + +public class KeyEventProcessService extends AccessibilityService implements AppStateManager.PointerStyleChangeListener { + @SuppressLint("HandlerLeak") // this can't be made static or it won't work + private final Handler mHandler = new Handler() { + @Override // android.os.Handler + public void handleMessage(Message message) { + switch (message.what) { + case 0: + KeyEventProcessService.this.mMousePointer.moveMouse(KeyEventProcessService.this.mOffsetX, KeyEventProcessService.this.mOffsetY); + return; + case 1: + KeyEventProcessService.this.updateMousePointerOffset((KeyEvent) message.obj); + KeyEventProcessService.this.animateMousePointer(); + return; + case 2: + KeyEventProcessService.this.injectMoveTouchEvent(); + return; + default: + return; + } + } + }; + private MousePointer mMousePointer; + private float mOffsetX; + private float mOffsetY; + private long mOkDownTime; + private Timer mTimer; + private TimerTask mTimerTask; + private CharSequence mUseWindowPackageName; + private static KeyEventProcessService instance; + + @Override // android.accessibilityservice.AccessibilityService + protected boolean onKeyEvent(KeyEvent event) { + int action = event.getAction(); + if (action == 1) { + cancelLongPress(); + } + boolean isIntercept = isNeedToInterceptNavigationKeyEvent(event) || isNeedToInterceptOKKeyEvent(event); + return isIntercept || super.onKeyEvent(event); + } + + @Override // android.accessibilityservice.AccessibilityService + public void onInterrupt() { + Log.i("MousePointer", "KeyEventProcessService::onInterrupt"); + } + + @Override // android.app.Service + public void onCreate() { + Log.i("MousePointer", "KeyEventProcessService::onCreate"); + super.onCreate(); + this.mMousePointer = new MousePointer(getApplicationContext()); + this.mMousePointer.hideMouse(); + PackageUtil.updateEnabled3rdPreloadedApps(getApplicationContext()); + } + + @Override // android.app.Service + public boolean onUnbind(Intent intent) { + Log.i("MousePointer", "KeyEventProcessService::onUnbind"); + return super.onUnbind(intent); + } + + public static KeyEventProcessService getInstance() { + return instance; + } + + @Override // android.accessibilityservice.AccessibilityService + protected void onServiceConnected() { + instance = this; + AppStateManager.setPointerStyleListener(this); + Log.i("MousePointer", "config success!"); + } + + @Override + public void onDestroy() { + instance = null; + AppStateManager.setPointerStyleListener(null); + super.onDestroy(); + } + + @Override // android.accessibilityservice.AccessibilityService + public void onAccessibilityEvent(AccessibilityEvent event) { + int eventType = event.getEventType(); + switch (eventType) { + case 16: + this.mMousePointer.hideMouse(); + return; + case 32: + Log.d("MousePointer", " TYPE_WINDOW_STATE_CHANGED " + ((Object) event.getPackageName())); + this.mUseWindowPackageName = event.getPackageName(); + updateMouseVisibility(); + updateAccessibilityService(); + return; + default: + return; + } + } + + private boolean isNeedToInterceptNavigationKeyEvent(KeyEvent keyEvent) { + int keyCode = keyEvent.getKeyCode(); + int action = keyEvent.getAction(); + if (!PackageUtil.shouldPointerBeHidden(this.mUseWindowPackageName, getApplicationContext()) && (keyCode == 19 || keyCode == 20 || keyCode == 21 || keyCode == 22)) { + this.mMousePointer.showMouse(); + updateMousePointerOffset(keyEvent); + if (!this.mMousePointer.isOnMouseMovementRangeBoundary(this.mOffsetX, this.mOffsetY)) { + if (action == 0) { + this.mMousePointer.moveMouse(this.mOffsetX, this.mOffsetY); + triggerLongPressDelayed(keyEvent); + } + Log.d("MousePointer", "isNeedToInterceptNavigationKeyEvent true"); + return true; + } else if ("com.facebook.lite".equals(this.mUseWindowPackageName)) { + InjectEventUtil.injectInputTouchEventForMove(getApplicationContext(), this.mOffsetY); + Log.d("MousePointer", "isNeedToInterceptNavigationKeyEvent scroll true"); + } + } + return false; + } + + private boolean isNeedToInterceptOKKeyEvent(KeyEvent keyEvent) { + int keyCode = keyEvent.getKeyCode(); + int action = keyEvent.getAction(); + if (this.mMousePointer.isShowing() && !PackageUtil.shouldPointerBeHidden(this.mUseWindowPackageName, getApplicationContext()) && keyCode == 66) { + if (action == 0) { + this.mOkDownTime = SystemClock.uptimeMillis(); + this.mHandler.removeMessages(2); + InjectEventUtil.injectDownTouchEvent(getApplicationContext(), this.mOkDownTime, this.mMousePointer.getMousePointerX(), this.mMousePointer.getMousePointerY()); + Log.d("MousePointer", "isNeedToInterceptOKKeyEvent down"); + this.mHandler.sendEmptyMessageDelayed(2, 30L); + } else if (action == 1) { + InjectEventUtil.injectUpTouchEvent(getApplicationContext(), this.mOkDownTime, this.mMousePointer.getMousePointerX(), this.mMousePointer.getMousePointerY()); + Log.d("MousePointer", "isNeedToInterceptOKKeyEvent up"); + } + return true; + } + return false; + } + + public void injectMoveTouchEvent() { + InjectEventUtil.injectMoveTouchEvent(getApplicationContext(), this.mOkDownTime, this.mMousePointer.getMousePointerX() + 3.0f, this.mMousePointer.getMousePointerY() + 3.0f); + } + + private void updateMouseVisibility() { + if (PackageUtil.shouldPointerBeHidden(this.mUseWindowPackageName, getApplicationContext())) { + this.mMousePointer.hideMouse(); + } + } + + private void triggerLongPressDelayed(KeyEvent event) { + cancelLongPress(); + Message message = Message.obtain(); + message.obj = event; + message.what = 1; + this.mHandler.sendMessageDelayed(message, 300L); + } + + private void cancelLongPress() { + stopAnimateMousePointer(); + this.mHandler.removeMessages(1); + } + + public void updateMousePointerOffset(KeyEvent event) { + if (event.getKeyCode() == 19) { + this.mOffsetX = 0.0f; + this.mOffsetY = -(PackageUtil.getDensity(getApplicationContext()) * 10.0f); + } else if (event.getKeyCode() == 20) { + this.mOffsetX = 0.0f; + this.mOffsetY = PackageUtil.getDensity(getApplicationContext()) * 10.0f; + } else if (event.getKeyCode() == 21) { + this.mOffsetX = -(PackageUtil.getDensity(getApplicationContext()) * 10.0f); + this.mOffsetY = 0.0f; + } else if (event.getKeyCode() == 22) { + this.mOffsetX = PackageUtil.getDensity(getApplicationContext()) * 10.0f; + this.mOffsetY = 0.0f; + } + } + + public void animateMousePointer() { + if (this.mOffsetX != 0.0f || this.mOffsetY != 0.0f) { + this.mTimer = new Timer(); + this.mTimerTask = new TimerTask() { // from class: org.eu.octt.supermousepointer.KeyEventProcessService.2 + @Override // java.util.TimerTask, java.lang.Runnable + public void run() { + KeyEventProcessService.this.mHandler.sendEmptyMessage(0); + } + }; + this.mTimer.schedule(this.mTimerTask, 0L, 10L); + } + } + + private void stopAnimateMousePointer() { + if (this.mTimer != null) { + this.mTimer.cancel(); + } + if (this.mTimerTask != null) { + this.mTimerTask.cancel(); + } + } + + private void updateAccessibilityService() { + AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo(); + accessibilityServiceInfo.eventTypes = -1; + accessibilityServiceInfo.feedbackType = 16; + accessibilityServiceInfo.notificationTimeout = 100L; + if (PackageUtil.shouldPointerBeHidden(this.mUseWindowPackageName, getApplicationContext())) { + accessibilityServiceInfo.flags = 1; + } else { + accessibilityServiceInfo.flags = 32; + } + setServiceInfo(accessibilityServiceInfo); + } + + @Override + public void onPointerStyleChanged(String resName) { + this.mMousePointer.setImageResource(AppStateManager.getPointerDrawableId(this, resName)); + } + + private AccessibilityNodeInfo trackedNode; + + public void setTrackedNode(AccessibilityNodeInfo node) { + if (trackedNode != null) trackedNode.recycle(); + trackedNode = AccessibilityNodeInfo.obtain(node); + } + + public AccessibilityNodeInfo getTrackedNode() { + return trackedNode != null ? AccessibilityNodeInfo.obtain(trackedNode) : null; + } + + public void clearTrackedNode() { + if (trackedNode != null) { + trackedNode.recycle(); + trackedNode = null; + } + } + +// public void updatePointerPosition(int x, int y) { +// runOnUiThread(() -> { +// FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mousePointer.getLayoutParams(); +// params.leftMargin = x; +// params.topMargin = y; +// mousePointer.setLayoutParams(params); +// }); +// } +} diff --git a/app/src/main/java/org/eu/octt/supermousepointer/MouserPointerReceiver.java b/app/src/main/java/org/eu/octt/supermousepointer/MouserPointerReceiver.java new file mode 100644 index 0000000..7e694d9 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/MouserPointerReceiver.java @@ -0,0 +1,36 @@ +package org.eu.octt.supermousepointer; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.util.Log; + +public class MouserPointerReceiver extends BroadcastReceiver { + @Override // android.content.BroadcastReceiver + public void onReceive(Context context, Intent intent) { + enableKeyEventProcessService(context); + } + + private void enableKeyEventProcessService(Context context) { + String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(), "enabled_accessibility_services"); + Log.d("MousePointer", "enableKeyEventProcessService Accessibility services :: " + enabledServicesSetting); + if (enabledServicesSetting == null || enabledServicesSetting.equals("")) { + updateAccessibilityServices("org.eu.octt.supermousepointer/com.qualcomm.qti.mousepointer.KeyEventProcessService", context); + } else if (!enabledServicesSetting.contains("org.eu.octt.supermousepointer/com.qualcomm.qti.mousepointer.KeyEventProcessService")) { + updateAccessibilityServices(enabledServicesSetting + ":org.eu.octt.supermousepointer/com.qualcomm.qti.mousepointer.KeyEventProcessService", context); + } + disableBootReceiver(context); + Log.d("MousePointer", "boot completed"); + } + + private void updateAccessibilityServices(String enabledServicesSetting, Context context) { + Settings.Secure.putString(context.getContentResolver(), "enabled_accessibility_services", enabledServicesSetting); + Settings.Secure.putInt(context.getContentResolver(), "accessibility_enabled", 1); + } + + private void disableBootReceiver(Context context) { + context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, MouserPointerReceiver.class), 2, 1); + } +} diff --git a/app/src/main/java/org/eu/octt/supermousepointer/MyApplication.java b/app/src/main/java/org/eu/octt/supermousepointer/MyApplication.java new file mode 100644 index 0000000..25cad58 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/MyApplication.java @@ -0,0 +1,11 @@ +package org.eu.octt.supermousepointer; + +import android.app.Application; + +public class MyApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + AppStateManager.initialize(this); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/eu/octt/supermousepointer/SettingsActivity.java b/app/src/main/java/org/eu/octt/supermousepointer/SettingsActivity.java new file mode 100644 index 0000000..b6b5a14 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/SettingsActivity.java @@ -0,0 +1,125 @@ +package org.eu.octt.supermousepointer; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SettingsActivity extends Activity { + private FrameLayout contentFrame; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + getActionBar().setTitle(R.string.settings_title); + contentFrame = findViewById(R.id.contentFrame); + showDefaultTab(); + + ((RadioGroup)findViewById(R.id.tabGroup)).setOnCheckedChangeListener((group, checkedId) -> { + if (checkedId == R.id.tabApps) { + showFragment(new AppListFragment()); + } else { + showFragment(new CursorStyleFragment()); + } + }); + } + + private void showDefaultTab() { + ((RadioButton)findViewById(R.id.tabApps)).setChecked(true); + showFragment(new AppListFragment()); + } + + private void showFragment(Fragment fragment) { + getFragmentManager().beginTransaction().replace(R.id.contentFrame, fragment).commit(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.settings_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_info) { + showAboutDialog(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + RadioGroup tabs = findViewById(R.id.tabGroup); + int currentTab = tabs.indexOfChild(tabs.findViewById(tabs.getCheckedRadioButtonId())); + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if(currentTab > 0) { + ((RadioButton)tabs.getChildAt(currentTab - 1)).setChecked(true); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if(currentTab < 1) { + ((RadioButton)tabs.getChildAt(currentTab + 1)).setChecked(true); + return true; + } + break; + } + return super.onKeyDown(keyCode, event); + } + + private void showAboutDialog() { + try { + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_about, null); + + ((ImageView)view.findViewById(R.id.appIcon)).setImageDrawable(getPackageManager().getApplicationIcon(getPackageName())); + ((TextView)view.findViewById(R.id.appName)).setText(getPackageManager().getApplicationLabel(getApplicationInfo())); + ((TextView)view.findViewById(R.id.appVersion)).setText("v" + pInfo.versionName); + ((TextView)view.findViewById(R.id.appDescription)).setText(R.string.accessibility_description); + + SpannableString links = new SpannableString("Official Pages:" + + "\nhttps://gitlab.com/octospacc/SuperMousePointer" + + "\nhttps://github.com/octospacc/SuperMousePointer" + + "\nhttps://gitea.it/octospacc/SuperMousePointer"); + Linkify.addLinks(links, Linkify.WEB_URLS); + TextView linksView = view.findViewById(R.id.links); + linksView.setText(links); + linksView.setMovementMethod(LinkMovementMethod.getInstance()); + + builder.setView(view).setPositiveButton(android.R.string.ok, null).show(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/eu/octt/supermousepointer/utils/InjectEventUtil.java b/app/src/main/java/org/eu/octt/supermousepointer/utils/InjectEventUtil.java new file mode 100644 index 0000000..745710e --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/utils/InjectEventUtil.java @@ -0,0 +1,254 @@ +package org.eu.octt.supermousepointer.utils; + +import android.accessibilityservice.AccessibilityService; +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.InputEvent; +import android.view.MotionEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import org.eu.octt.supermousepointer.KeyEventProcessService; + +import java.lang.reflect.Field; +import java.security.Key; +import java.util.ArrayDeque; +import java.util.List; + +public class InjectEventUtil { + public static void injectDownTouchEvent(Context context, long downTime, float x, float y) { + injectTouchEvent(context, downTime, SystemClock.uptimeMillis(), 0, x, y); + } + + public static void injectMoveTouchEvent(Context context, long downTime, float x, float y) { + injectTouchEvent(context, downTime, SystemClock.uptimeMillis(), 2, x, y); + } + + public static void injectUpTouchEvent(Context context, long downTime, float x, float y) { + injectTouchEvent(context, downTime, SystemClock.uptimeMillis(), 1, x, y); + } + + public static void injectInputTouchEventForMove(Context context, float offsetY) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + long downTime = SystemClock.uptimeMillis(); + injectTouchEvent(context, downTime, downTime, 0, dm.widthPixels / 2, dm.heightPixels / 2); + injectTouchEvent(context, downTime, downTime + 20, 2, dm.widthPixels / 2, (dm.heightPixels / 2) - offsetY); + injectTouchEvent(context, downTime, downTime + 20, 1, dm.widthPixels / 2, (dm.heightPixels / 2) - offsetY); + } + + private static void injectTouchEvent(Context context, long downTime, long eventTime, + int action, float x, float y) { + + var service = KeyEventProcessService.getInstance(); + if (service == null) return; + + switch (action) { + case MotionEvent.ACTION_DOWN: + handleTouchDown(service, x, y); + break; + case MotionEvent.ACTION_MOVE: + handleTouchMove(service, x, y); + break; + case MotionEvent.ACTION_UP: + handleTouchUp(service); + break; + } + } + + private static void handleTouchDown(KeyEventProcessService service, float x, float y) { + updateLastCoordinates(x, y); + AccessibilityNodeInfo root = service.getRootInActiveWindow(); + if (root != null) { + AccessibilityNodeInfo node = findDeepestNodeAt(root, (int) x, (int) y); + if (node != null) { + service.setTrackedNode(node); + node.recycle(); + } + root.recycle(); + } + } + + private static float lastX, lastY; + + public static void updateLastCoordinates(float x, float y) { + lastX = x; + lastY = y; + } + + private static void handleTouchMove(KeyEventProcessService service, float x, float y) { +// AccessibilityNodeInfo current = service.getTrackedNode(); +// if (current != null) { +// if (current.isScrollable()) { +// current.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); +// } +// current.recycle(); +// } + updateLastCoordinates(x, y); + } + + private static void handleTouchUp(KeyEventProcessService service) { + boolean clickSuccess = false; + AccessibilityNodeInfo node = service.getTrackedNode(); + if (node != null) { + if (node.isClickable()) { + clickSuccess = node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + } + service.clearTrackedNode(); + node.recycle(); + } + if (!clickSuccess) { + try { + Class argsClass = AccessibilityNodeInfo.class; + @SuppressLint("PrivateApi") + Field xField = argsClass.getDeclaredField("ACTION_ARGUMENT_X_INT"); + @SuppressLint("PrivateApi") + Field yField = argsClass.getDeclaredField("ACTION_ARGUMENT_Y_INT"); + + Bundle args = new Bundle(); + args.putInt((String)xField.get(null), (int)lastX); + args.putInt((String)yField.get(null), (int)lastY); + + AccessibilityNodeInfo root = service.getRootInActiveWindow(); + if (root != null) { + root.performAction(AccessibilityNodeInfo.ACTION_CLICK, args); + root.recycle(); + } + } catch (ReflectiveOperationException e) {} + } + } + +// private static void injectTouchEvent(Context context, long downTime, long eventTime, int action, float x, float y) { +//// MotionEvent motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0); +//// motionEvent.setSource(4098); +//// InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE); +//// try { +//// im.getClass().getMethod("injectInputEvent", InputEvent.class, int.class).invoke(im, motionEvent, 0); +//// } catch (ReflectiveOperationException e) { +//// throw new RuntimeException(e); +//// } +// +// var service = KeyEventProcessService.getInstance(); +// if (service == null) { +// Log.e("InjectTouch", "Accessibility service not available"); +// return; +// } +// +// try { +// AccessibilityNodeInfo rootNode = service.getRootInActiveWindow(); +// if (rootNode == null) { +// Log.e("InjectTouch", "No root node available"); +// return; +// } +// +// // Find the deepest node at coordinates +// AccessibilityNodeInfo targetNode = findDeepestNodeAt(rootNode, (int) x, (int) y); +// if (targetNode == null) { +// Log.e("InjectTouch", "No node found at coordinates (" + x + "," + y + ")"); +// rootNode.recycle(); +// return; +// } +// +// // Verify node is visible and actionable +// if (!isNodeActionable(targetNode)) { +// Log.e("InjectTouch", "Node not actionable: " + targetNode); +// targetNode.recycle(); +// rootNode.recycle(); +// return; +// } +// +// // Perform the click +// boolean result = performNodeClick(targetNode); +// Log.d("InjectTouch", (result ? "Success" : "Failed") + " clicking node: " + targetNode); +// +// targetNode.recycle(); +// rootNode.recycle(); +// +// } catch (Exception e) { +// Log.e("InjectTouch", "Error handling touch event", e); +// } +// } + + private static AccessibilityNodeInfo findDeepestNodeAt(AccessibilityNodeInfo root, int x, int y) { + if (root == null) return null; + + ArrayDeque queue = new ArrayDeque<>(); + queue.add(root); + AccessibilityNodeInfo deepestNode = null; + int maxDepth = -1; + + while (!queue.isEmpty()) { + AccessibilityNodeInfo current = queue.poll(); + try { + Rect bounds = new Rect(); + current.getBoundsInScreen(bounds); + + if (bounds.contains(x, y)) { + // Track deepest node + int depth = getNodeDepth(current); + if (depth > maxDepth) { + if (deepestNode != null) deepestNode.recycle(); + deepestNode = AccessibilityNodeInfo.obtain(current); + maxDepth = depth; + } + + // Add children to queue + for (int i = 0; i < current.getChildCount(); i++) { + AccessibilityNodeInfo child = current.getChild(i); + if (child != null) { + queue.add(child); + } + } + } + } finally { + if (current != root) { + current.recycle(); + } + } + } + return deepestNode; + } + + private static int getNodeDepth(AccessibilityNodeInfo node) { + int depth = 0; + while (node != null) { + depth++; + node = node.getParent(); + } + return depth; + } + + private static boolean isNodeActionable(AccessibilityNodeInfo node) { + if (node == null) return false; + + return (node.isClickable() || node.isLongClickable() || node.isFocusable()) + && node.isEnabled() + && node.isVisibleToUser(); + } + + private static boolean performNodeClick(AccessibilityNodeInfo node) { + if (node.isClickable()) { + return node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + } + + // Try parents up the hierarchy + AccessibilityNodeInfo parent = node.getParent(); + while (parent != null) { + if (parent.isClickable()) { + boolean result = parent.performAction(AccessibilityNodeInfo.ACTION_CLICK); + parent.recycle(); + return result; + } + AccessibilityNodeInfo oldParent = parent; + parent = parent.getParent(); + oldParent.recycle(); + } + + // Fallback to accessibility focus + return node.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) && node.performAction(AccessibilityNodeInfo.ACTION_CLICK); + } +} diff --git a/app/src/main/java/org/eu/octt/supermousepointer/utils/PackageUtil.java b/app/src/main/java/org/eu/octt/supermousepointer/utils/PackageUtil.java new file mode 100644 index 0000000..dfb7898 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/utils/PackageUtil.java @@ -0,0 +1,76 @@ +package org.eu.octt.supermousepointer.utils; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; + +import org.eu.octt.supermousepointer.AppStateManager; + +import java.util.Arrays; +import java.util.List; + +public class PackageUtil { + private static List mEnabled3rdPreloadedApps = Arrays.asList("com.facebook.lite", "com.facebook.lite.stub", "com.facebook.system", "com.facebook.appmanager", "com.whatsapp"); + private static List mDisabled3rdApps = Arrays.asList("com.borqs.ime", "com.android.cts.verifier", "ademar.textlauncher", "com.mixplorer"); + + private static boolean isSystemAppInTop(Context context) { + ActivityManager am = (ActivityManager) context.getSystemService("activity"); + ComponentName cn = am.getRunningTasks(1).get(0).topActivity; + try { + PackageManager pm = context.getPackageManager(); + ApplicationInfo info = pm.getApplicationInfo(cn.getPackageName(), 0); + return (info.flags & 1) != 0; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return true; + } + } + + public static boolean shouldPointerBeHidden(CharSequence useWindowPackageName, Context context) { + String packageName = (String) useWindowPackageName; + + switch (AppStateManager.getState(packageName)) { + case AppStateManager.AppState.WHITELISTED: + return false; + case AppStateManager.AppState.BLACKLISTED: + return true; + case AppStateManager.AppState.DEFAULT: + default: + return !mEnabled3rdPreloadedApps.contains(packageName) && (isSystemAppInTop(context) || mDisabled3rdApps.contains(packageName) || context.getPackageName().equals(packageName)); + } + } + + public static void updateEnabled3rdPreloadedApps(Context context) { + List apps; + String enabled3rdPreloadedApps = Settings.System.getString(context.getContentResolver(), "enabled_3rd_preloaded_apps"); + if (enabled3rdPreloadedApps != null && !enabled3rdPreloadedApps.equals("") && (apps = Arrays.asList(enabled3rdPreloadedApps.split("\\|"))) != null && apps.size() > 0) { + for (String s : apps) { + if (!mEnabled3rdPreloadedApps.contains(s)) { + mEnabled3rdPreloadedApps.add(s); + } else { + Log.d("MousePointer", "duplicate s == " + s); + } + } + Log.d("MousePointer", " updateEnabled3rdPreloadedApps mEnabled3rdPreloadedApps :: " + enabled3rdPreloadedApps + " mEnabled3rdPreloadedApps size :: " + mEnabled3rdPreloadedApps.size()); + } + } + + public static float getDensity(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.density; + } + + public static int getStatusBarHeight(Context context) { + int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resId <= 0) { + return 0; + } + int statusBarHeight = context.getResources().getDimensionPixelSize(resId); + return statusBarHeight; + } +} diff --git a/app/src/main/java/org/eu/octt/supermousepointer/window/MousePointer.java b/app/src/main/java/org/eu/octt/supermousepointer/window/MousePointer.java new file mode 100644 index 0000000..814e850 --- /dev/null +++ b/app/src/main/java/org/eu/octt/supermousepointer/window/MousePointer.java @@ -0,0 +1,163 @@ +package org.eu.octt.supermousepointer.window; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.eu.octt.supermousepointer.AppStateManager; +import org.eu.octt.supermousepointer.R; +import org.eu.octt.supermousepointer.utils.PackageUtil; + +public class MousePointer { + private Context mContext; + private int mMaxMousePointerX; + private int mMaxMousePointerY; + private int mScreenHeight; + private int mScreenWidth; + private int mStatusBarHeight; + private WindowManager mWM; + private ImageView mouseImg; + private FrameLayout mousePointerLayout; + + private void initMouse() { + initMouseRelatedParams(); + initMousePointerWindow(); + } + + private void initMousePointerWindow() { + this.mWM = (WindowManager) this.mContext.getSystemService("window"); + this.mousePointerLayout = new FrameLayout(this.mContext); + this.mousePointerLayout.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + )); + this.mouseImg = new ImageView(this.mContext); + this.mouseImg.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + )); + this.mouseImg.setImageResource(AppStateManager.getPointerDrawableId(mContext, AppStateManager.getSelectedPointerStyle())); + this.mousePointerLayout.addView(this.mouseImg); + this.mouseImg.setClickable(false); + this.mouseImg.setFocusable(false); + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + params.height = -1; + params.width = -1; + params.flags = 24; + params.format = -3; + params.type = 2010; + params.gravity = 51; + this.mWM.addView(this.mousePointerLayout, params); + } + + private void initMouseRelatedParams() { + DisplayMetrics dm = this.mContext.getResources().getDisplayMetrics(); + this.mStatusBarHeight = PackageUtil.getStatusBarHeight(this.mContext); + this.mScreenWidth = dm.widthPixels; + this.mScreenHeight = dm.heightPixels; + this.mMaxMousePointerX = this.mScreenWidth - ((int) (PackageUtil.getDensity(this.mContext) * 10.0f)); + this.mMaxMousePointerY = (this.mScreenHeight - this.mStatusBarHeight) - ((int) (PackageUtil.getDensity(this.mContext) * 10.0f)); + Log.d("MousePointer", "initMouseRelatedParams ScreenWidth == " + dm.widthPixels + " ScreenHeight == " + dm.heightPixels + " mMaxMousePointerX == " + this.mMaxMousePointerX + " mMaxMousePointerY == " + this.mMaxMousePointerY + " mStatusBarHeight == " + this.mStatusBarHeight + " density == " + dm.density + " densityDpi == " + dm.densityDpi); + } + + private boolean isInMouseMovementRange() { + float mousePointerX = this.mouseImg.getTranslationX(); + float mousePointerY = this.mouseImg.getTranslationY(); + return mousePointerX <= ((float) this.mMaxMousePointerX) && mousePointerX >= 0.0f && mousePointerY <= ((float) this.mMaxMousePointerY) && mousePointerY >= 0.0f; + } + + private void moveMouseX(float relX) { + float targetX = this.mouseImg.getTranslationX() + relX; + if (targetX > this.mMaxMousePointerX) { + this.mouseImg.setTranslationX(this.mMaxMousePointerX); + } else if (targetX < 0.0f) { + this.mouseImg.setTranslationX(0.0f); + } else { + this.mouseImg.setTranslationX(targetX); + } + } + + private void moveMouseY(float relY) { + float targetY = this.mouseImg.getTranslationY() + relY; + if (targetY > this.mMaxMousePointerY) { + this.mouseImg.setTranslationY(this.mMaxMousePointerY); + } else if (targetY < 0.0f) { + this.mouseImg.setTranslationY(0.0f); + } else { + this.mouseImg.setTranslationY(targetY); + } + } + + private boolean isStatusBarShowing() { + int[] top = new int[2]; + this.mouseImg.getLocationOnScreen(top); + return ((float) top[1]) > this.mouseImg.getTranslationY(); + } + + private boolean needToRefreshMouseMovementRange() { + return this.mouseImg.getTranslationY() >= ((float) ((this.mScreenHeight - this.mStatusBarHeight) - ((int) (10.0f * PackageUtil.getDensity(this.mContext))))); + } + + private void updateMouseRangeMaxY() { + if (needToRefreshMouseMovementRange()) { + if (isStatusBarShowing()) { + this.mMaxMousePointerY = (this.mScreenHeight - this.mStatusBarHeight) - ((int) (PackageUtil.getDensity(this.mContext) * 10.0f)); + } else { + this.mMaxMousePointerY = this.mScreenHeight - ((int) (PackageUtil.getDensity(this.mContext) * 10.0f)); + } + Log.e("MousePointer", "updateMouseRangeMaxY mMaxMousePointerY = " + this.mMaxMousePointerY); + } + if (this.mouseImg.getTranslationY() > this.mMaxMousePointerY) { + this.mouseImg.setTranslationY(this.mMaxMousePointerY); + } + } + + public MousePointer(Context context) { + this.mContext = context; + initMouse(); + } + + public void showMouse() { + this.mousePointerLayout.setVisibility(0); + } + + public void hideMouse() { + this.mousePointerLayout.setVisibility(8); + } + + public boolean isShowing() { + return this.mousePointerLayout.getVisibility() == 0; + } + + public void moveMouse(float relX, float relY) { + Log.d("MousePointer", "moveMouse == relX == " + relX + " relY == " + relY + " translateX == " + this.mouseImg.getTranslationX() + " translateY == " + this.mouseImg.getTranslationY()); + updateMouseRangeMaxY(); + if (isInMouseMovementRange()) { + moveMouseX(relX); + moveMouseY(relY); + } + } + + public boolean isOnMouseMovementRangeBoundary(float offsetX, float offsetY) { + return (offsetY > 0.0f && this.mouseImg.getTranslationY() == ((float) this.mMaxMousePointerY)) || (offsetY < 0.0f && this.mouseImg.getTranslationY() == 0.0f) || ((offsetX > 0.0f && this.mouseImg.getTranslationX() == ((float) this.mMaxMousePointerX)) || (offsetX < 0.0f && this.mouseImg.getTranslationX() == 0.0f)); + } + + public float getMousePointerX() { + return this.mouseImg.getTranslationX(); + } + + public float getMousePointerY() { + int[] top = new int[2]; + this.mouseImg.getLocationOnScreen(top); + return top[1]; + } + + public void setImageResource(int newDrawableId) { + this.mouseImg.setImageResource(newDrawableId); + } +} diff --git a/app/src/main/res/drawable-hdpi/pointer_arrow.png b/app/src/main/res/drawable-hdpi/pointer_arrow.png new file mode 100644 index 0000000..60d5e2c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/pointer_arrow.png differ diff --git a/app/src/main/res/drawable-hdpi/pointer_arrow_app.png b/app/src/main/res/drawable-hdpi/pointer_arrow_app.png new file mode 100644 index 0000000..9b755bb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/pointer_arrow_app.png differ diff --git a/app/src/main/res/drawable-hdpi/pointer_arrow_inverted.png b/app/src/main/res/drawable-hdpi/pointer_arrow_inverted.png new file mode 100644 index 0000000..3554eb6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/pointer_arrow_inverted.png differ diff --git a/app/src/main/res/drawable/ic_state_blacklist.xml b/app/src/main/res/drawable/ic_state_blacklist.xml new file mode 100644 index 0000000..fc8405b --- /dev/null +++ b/app/src/main/res/drawable/ic_state_blacklist.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_state_default.xml b/app/src/main/res/drawable/ic_state_default.xml new file mode 100644 index 0000000..f3523df --- /dev/null +++ b/app/src/main/res/drawable/ic_state_default.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_state_whitelist.xml b/app/src/main/res/drawable/ic_state_whitelist.xml new file mode 100644 index 0000000..8817d06 --- /dev/null +++ b/app/src/main/res/drawable/ic_state_whitelist.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..5ba8b5b --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml new file mode 100644 index 0000000..9b6d7ae --- /dev/null +++ b/app/src/main/res/layout/dialog_about.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_app_list.xml b/app/src/main/res/layout/fragment_app_list.xml new file mode 100644 index 0000000..0beaac1 --- /dev/null +++ b/app/src/main/res/layout/fragment_app_list.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cursor_style.xml b/app/src/main/res/layout/fragment_cursor_style.xml new file mode 100644 index 0000000..ec2ef88 --- /dev/null +++ b/app/src/main/res/layout/fragment_cursor_style.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_app.xml b/app/src/main/res/layout/list_item_app.xml new file mode 100644 index 0000000..c81b96e --- /dev/null +++ b/app/src/main/res/layout/list_item_app.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_cursor_style.xml b/app/src/main/res/layout/list_item_cursor_style.xml new file mode 100644 index 0000000..dfdfb4f --- /dev/null +++ b/app/src/main/res/layout/list_item_cursor_style.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/settings_menu.xml b/app/src/main/res/menu/settings_menu.xml new file mode 100644 index 0000000..96e5642 --- /dev/null +++ b/app/src/main/res/menu/settings_menu.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..98b315f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..2967c76 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..c61a25c Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..90d0b22 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..cccc6e0 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-hdpi/drawables.xml b/app/src/main/res/values-hdpi/drawables.xml new file mode 100644 index 0000000..045e125 --- /dev/null +++ b/app/src/main/res/values-hdpi/drawables.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..7c3f822 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,7 @@ + + + SuperMousePointer (Impostazioni) + Un mouse virtuale per Android legacy che può essere usato nelle app desiderate con le freccette di navigazione. + App + Cursore + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/public.xml b/app/src/main/res/values/public.xml new file mode 100644 index 0000000..604f1fd --- /dev/null +++ b/app/src/main/res/values/public.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..ce183ec --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + + SuperMousePointer + SuperMousePointer (Settings) + A virtual mouse for legacy Android which can be used in desired apps via the arrow navigation keys. + Info + Default + Whitelist + Blacklist + Apps + Cursor + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..06fe889 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/keyevent_service_config.xml b/app/src/main/res/xml/keyevent_service_config.xml new file mode 100644 index 0000000..12f5ace --- /dev/null +++ b/app/src/main/res/xml/keyevent_service_config.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..565f8c2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,4 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { +alias(libs.plugins.android.application) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..4387edc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..a1d830e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +[versions] +agp = "8.7.1" +junit = "4.13.2" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +appcompat = "1.7.0" +material = "1.12.0" + +[libraries] +junit = { group = "junit", name = "junit", version.ref = "junit" } +ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..94ea999 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 14 08:35:51 CET 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..59b1b75 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "SuperMousePointer" +include ':app'