diff --git a/.gitignore b/.gitignore index ba87831..97b38b5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ /captures .externalNativeBuild .cxx +app/libs/com.lge.ivi.jar app/libs/com.lge.ivi_191209.jar diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..146ab09 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index dce025c..fec24d1 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,34 @@ - # App Manager -Allows you to map hardware keys to custom apps and manage apps.
-**Features:
** -* Map long press of hardware keys (except Settings) to any installed app.
-* Map back key event to an hardware button, doesn't require restored stock apps on 191209 software.
+Allows you to map hardware keys to custom apps and manage apps. + +## Features: + +* Map long press of hardware keys (except Settings) to any installed app. +* Map double press of hardware keys to any installed app. +* Map back key event to an hardware button, doesn't require restored stock apps on 191209 software. +* Map EV key, ONLY DOUBLE PRESS IS WORKING. * Clear app cache * Clear app data * Force stop app * Uninstall app +* Restart service when Android OOM kills it -**Long press settings (gearwheel) button to launch the app. Before launching it the first time push the settings button shortly and long press it afterwards. This is only required after installing the app for the first time.
** +Both long press and double press does not start the original application for that button just +the app you programmed. Except EV button, which I cannot stop starting original EV app first. -![Screenshot](doc/screenshot.png)
+**Long press settings (gearwheel) button to launch the app. Before launching it the first time push +the settings button shortly and long press it afterwards. This is only required after installing the +app for the first time.** + +![Screenshot](doc/screenshot.png) Checkbox for list item will show you if you have mapped a key already. -![Screenshot](doc/screenshot2.png)
+![Screenshot](doc/screenshot2.png) -If you like my work I'd be happy if you buy me a coffee. Thanks!
+If you like my work I'd be happy if you buy me a coffee. Thanks! [![](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RT8WTFDGMLFPG) + +### Translations +* English +* Hungarian diff --git a/app/build.gradle b/app/build.gradle index 192d320..f4b6e52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "g4rb4g3.at.ctsteststarter" minSdkVersion 17 targetSdkVersion 17 - versionCode 206 - versionName "2.0.6" + versionCode 207 + versionName "2.0.7" } buildTypes { release { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 63be1d6..dcaf27e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ @@ -28,9 +29,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/lge/ivi/server/ExtMediaService.java b/app/src/main/java/com/lge/ivi/server/ExtMediaService.java index 210d936..78199c0 100644 --- a/app/src/main/java/com/lge/ivi/server/ExtMediaService.java +++ b/app/src/main/java/com/lge/ivi/server/ExtMediaService.java @@ -142,6 +142,11 @@ public boolean removeFileInSd(String s) throws RemoteException { throw new UnsupportedOperationException(); } + @Override + public boolean removeFileInUsb(String s) throws RemoteException { + throw new UnsupportedOperationException(); + } + @Override public boolean writeFileInSd(String s, String s1) throws RemoteException { throw new UnsupportedOperationException(); diff --git a/app/src/main/java/g4rb4g3/at/ctsteststarter/BroadcastsReceiver.java b/app/src/main/java/g4rb4g3/at/ctsteststarter/BroadcastsReceiver.java index 171a63a..d6f9423 100644 --- a/app/src/main/java/g4rb4g3/at/ctsteststarter/BroadcastsReceiver.java +++ b/app/src/main/java/g4rb4g3/at/ctsteststarter/BroadcastsReceiver.java @@ -10,3 +10,6 @@ public void onReceive(Context context, Intent intent) { context.startService(new Intent(context, KeyInterceptorService.class)); } } + + + diff --git a/app/src/main/java/g4rb4g3/at/ctsteststarter/EVButtonReceiver.java b/app/src/main/java/g4rb4g3/at/ctsteststarter/EVButtonReceiver.java new file mode 100644 index 0000000..5993ab1 --- /dev/null +++ b/app/src/main/java/g4rb4g3/at/ctsteststarter/EVButtonReceiver.java @@ -0,0 +1,22 @@ +package g4rb4g3.at.ctsteststarter; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; +import android.view.KeyEvent; + + +public class EVButtonReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, Intent intent) { + KeyInterceptorService service = KeyInterceptorService.self; + // Simulate keypress with an unknown code + try { + service.mKeyInterceptor.onKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyInterceptorService.KEYCODE_EV)); + } catch (RemoteException e) { + e.printStackTrace(); + } + abortBroadcast(); + } +} \ No newline at end of file diff --git a/app/src/main/java/g4rb4g3/at/ctsteststarter/KeyInterceptorService.java b/app/src/main/java/g4rb4g3/at/ctsteststarter/KeyInterceptorService.java index da5a490..8546061 100644 --- a/app/src/main/java/g4rb4g3/at/ctsteststarter/KeyInterceptorService.java +++ b/app/src/main/java/g4rb4g3/at/ctsteststarter/KeyInterceptorService.java @@ -9,6 +9,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.view.KeyEvent; @@ -25,207 +26,306 @@ import androidx.core.app.NotificationCompat; public class KeyInterceptorService extends Service { - public static final String PREFERENCES_NAME = "preferences"; + public static KeyInterceptorService self; + + public static final String PREFERENCES_NAME = "preferences"; + + public static final int SHOW_MESSAGE = 1; + public static final int MAPPED_APP = 2; + public static final int UNMAPPED_APP = 3; + + public static final int DBL_TIMEOUT = 300; // ms + public static final int DBL_TIMEOUT_EV = 2000; // ms + + public static final int KEYCODE_EV = 65535; + + private final IBinder mBinder = new KeyInterceptorBinder(); + private final List mRegisteredHandlers = new ArrayList<>(); + private SharedPreferences mSharedPreferences; + private ApplicationInfo mNextAppMappingApplicationInfo; + private boolean mClearKeyMapping = false; + private boolean mActivityVisible = false; + private boolean mMapBackKey = false; + private boolean mMapRecentApps = false; + + // Double keypress + private static int sCalls = 0; + private static long sLastCall = 0; + private static int sLastKey = 0; + private final Handler handler = new Handler(Looper.getMainLooper()); + private final Runnable runnable = () -> { + if (sLastKey != 0) { + // Resend the key if it was not a double keypress + injectKeyEvent(sLastKey); + } + }; + + private boolean _clearDbl() { + handler.removeCallbacks(runnable); + sLastKey = 0; + sCalls = 0; + sLastCall = 0; + return (mNextAppMappingApplicationInfo != null || mClearKeyMapping || mMapBackKey || mMapRecentApps); + } - public static final int SHOW_MESSAGE = 1; - public static final int MAPPED_APP = 2; - public static final int UNMAPPED_APP = 3; + public IKeyInterceptor.Stub mKeyInterceptor = new IKeyInterceptor.Stub() { + @Override + public boolean onKeyEvent(KeyEvent keyEvent) { + String key; + + if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + + int to = keyEvent.getKeyCode() == KEYCODE_EV ? DBL_TIMEOUT_EV : DBL_TIMEOUT; // More time if EV button + + if (keyEvent.isLongPress()) { + if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_SETTINGS) { + if (!mActivityVisible) { + startActivity(new Intent(getApplicationContext(), MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } else if (mMapBackKey || mClearKeyMapping || mNextAppMappingApplicationInfo != null) { + notifyHandlers(SHOW_MESSAGE, getString(R.string.settings_cannot_be_mapped)); + cancel(); + } + return true; + } + + key = String.valueOf(keyEvent.getKeyCode()); + } + else { + key = keyEvent.getKeyCode() + "_dbl"; + + // It should be a resended key event + if (sLastCall > 0 && System.currentTimeMillis() - sLastCall >= to) { + return _clearDbl(); + } + + // If not mapping check if we have a registered application + if (mNextAppMappingApplicationInfo == null && !mClearKeyMapping && !mMapBackKey + && !mMapRecentApps) { + String packageName = mSharedPreferences.getString(key, null); + // If there is no such app, we can send keycode immediately (no delay) + if (packageName == null) return _clearDbl(); + } + + // If the new key is not the same as the previous, then send it immediately + if (sLastKey != 0 && sLastKey != keyEvent.getKeyCode()) return _clearDbl(); + + sLastKey = keyEvent.getKeyCode(); + sLastCall = System.currentTimeMillis(); + // Resend keykode later to make single presses works as well though a little delayed + handler.postDelayed(runnable, to); + + // Count clicks + sCalls++; + if (sCalls == 2) { + // Go back to app manager if it was EV button + if (sLastKey == KEYCODE_EV) { + Intent intent = new Intent(getApplicationContext(), MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // You need this if starting + // the activity from a service + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + startActivity(intent); + } + // It is a double click + _clearDbl(); + } + // No process keypress further until we know it is a single or double keypress + else return true; + } + + // If it is nor double nor long keypress + if (key.length() == 0) return false; + + // Here we know if it is a double or single keypress + + // Mapping in progress a + if (mNextAppMappingApplicationInfo != null) { + mSharedPreferences.edit().putString(key, mNextAppMappingApplicationInfo.packageName).apply(); + notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_app_completed, key, mNextAppMappingApplicationInfo.name)); + notifyHandlers(MAPPED_APP, mNextAppMappingApplicationInfo.packageName); + mNextAppMappingApplicationInfo = null; + return true; + } + + if (mClearKeyMapping) { + String packageName = mSharedPreferences.getString(key, null); + if (packageName != null) { + notifyHandlers(UNMAPPED_APP, packageName); + mSharedPreferences.edit().remove(key).apply(); + notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_cleared, key)); + } + mClearKeyMapping = false; + return true; + } + + if (mMapBackKey) { + mSharedPreferences.edit().putString(key, "back_key").apply(); + notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_back_completed, key)); + mMapBackKey = false; + return true; + } + + if (mMapRecentApps) { + mSharedPreferences.edit().putString(key, "recent_apps").apply(); + notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_recent_completed, key)); + mMapRecentApps = false; + return true; + } + + String packageName = mSharedPreferences.getString(key, null); + if (packageName == null) { + return false; + } + switch (packageName) { + case "back_key": + injectKeyEvent(KeyEvent.KEYCODE_BACK); + break; + case "recent_apps": + openRecentApps(); + break; + default: + Context ctx = getApplicationContext(); + ctx.startActivity(ctx.getPackageManager().getLaunchIntentForPackage(packageName)); + } + + return true; + } + }; + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } - private final IBinder mBinder = new KeyInterceptorBinder(); - private List mRegisteredHandlers = new ArrayList<>(); - private SharedPreferences mSharedPreferences; - private ApplicationInfo mNextAppMappingApplicationInfo; - private boolean mClearKeyMapping = false; - private boolean mActivityVisible = false; - private boolean mMapBackKey = false; - private boolean mMapRecentApps = false; - private Context mContext; + @Override + public void onCreate() { + self = this; // Maybe this is not the most beautiful solution + + Notification notification = new NotificationCompat.Builder(this, "CtsTestStarterKeyInterceptorService") + .setContentTitle(getString(R.string.app_name)) + .setSmallIcon(R.mipmap.ic_launcher) + .setPriority(NotificationCompat.PRIORITY_MAX) + .build(); + startForeground(1, notification); + + mSharedPreferences = getApplicationContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); + + try { + IKeyService keyService = getKeyService(); + assert keyService != null; + keyService.setKeyInterceptor(mKeyInterceptor); + } catch (Exception e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; // Try to make the OS restart the service + } - private IKeyInterceptor.Stub mKeyInterceptor = new IKeyInterceptor.Stub() { @Override - public boolean onKeyEvent(KeyEvent keyEvent) throws RemoteException { - if (keyEvent.getAction() != KeyEvent.ACTION_DOWN || !keyEvent.isLongPress()) { - return false; - } - - if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_SETTINGS) { - if (!mActivityVisible) { - startActivity(new Intent(mContext, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - } else if (mMapBackKey || mClearKeyMapping || mNextAppMappingApplicationInfo != null) { - notifyHandlers(SHOW_MESSAGE, getString(R.string.settings_cannot_be_mapped)); - cancel(); + public void onDestroy() { + self = null; + super.onDestroy(); + // Try to restart immediately if OS kills it + Intent broadcastIntent = new Intent(this, ServiceRestartBroadcastReceiver.class); + sendBroadcast(broadcastIntent); + } + + private IKeyService getKeyService() { + try { + Method method = Class.forName("android.os.ServiceManager").getMethod("getService", String.class); + IBinder binder = (IBinder) method.invoke(null, "com.lge.ivi.server.Key"); + if (binder != null) { + return IKeyService.Stub.asInterface(binder); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); } - return true; - } + return null; + } + + public void registerHandler(Handler handler) { + mRegisteredHandlers.add(handler); + } + + public void unregisterHandler(Handler handler) { + mRegisteredHandlers.remove(handler); + } + + public void mapAppToKey(ApplicationInfo applicationInfo) { + mNextAppMappingApplicationInfo = applicationInfo; + } + + public void mapBackKey() { + mMapBackKey = true; + } - if (mNextAppMappingApplicationInfo != null) { - mSharedPreferences.edit().putString(String.valueOf(keyEvent.getKeyCode()), mNextAppMappingApplicationInfo.packageName).commit(); - notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_app_completed, keyEvent.getKeyCode(), mNextAppMappingApplicationInfo.name)); - notifyHandlers(MAPPED_APP, mNextAppMappingApplicationInfo.packageName); + public void clearKeyMapping() { + mClearKeyMapping = true; + } + + public void mapRecentApps() { + mMapRecentApps = true; + } + + public void cancel() { mNextAppMappingApplicationInfo = null; - return true; - } - - if (mClearKeyMapping) { - String packageName = mSharedPreferences.getString(String.valueOf(keyEvent.getKeyCode()), null); - if (packageName != null) { - notifyHandlers(UNMAPPED_APP, packageName); - mSharedPreferences.edit().remove(String.valueOf(keyEvent.getKeyCode())).commit(); - notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_cleared, keyEvent.getKeyCode())); - } + mMapBackKey = false; mClearKeyMapping = false; - return true; - } + mMapRecentApps = false; + } - if (mMapBackKey) { - mSharedPreferences.edit().putString(String.valueOf(keyEvent.getKeyCode()), "back_key").commit(); - notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_back_completed, keyEvent.getKeyCode())); - mMapBackKey = false; - return true; - } + public void setActivityVisible(boolean visible) { + mActivityVisible = visible; + } - if (mMapRecentApps) { - mSharedPreferences.edit().putString(String.valueOf(keyEvent.getKeyCode()), "recent_apps").commit(); - notifyHandlers(SHOW_MESSAGE, getString(R.string.mapping_recent_completed, keyEvent.getKeyCode())); - mMapRecentApps = false; - return true; - } - - String packageName = mSharedPreferences.getString(String.valueOf(keyEvent.getKeyCode()), null); - if (packageName == null) { - return false; - } - switch (packageName) { - case "back_key": - injectKeyEvent(KeyEvent.KEYCODE_BACK); - return true; - case "recent_apps": - openRecentApps(); - return true; - default: - mContext.startActivity(mContext.getPackageManager().getLaunchIntentForPackage(packageName)); - return true; - } - } - }; - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onCreate() { - Notification notification = new NotificationCompat.Builder(this, null) - .setContentTitle(getString(R.string.app_name)) - .setSmallIcon(R.mipmap.ic_launcher) - .setPriority(NotificationCompat.PRIORITY_MAX) - .build(); - startForeground(1, notification); - - mContext = getApplicationContext(); - mSharedPreferences = mContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); - - try { - IKeyService keyService = getKeyService(); - keyService.setKeyInterceptor(mKeyInterceptor); - } catch (Exception e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - - private IKeyService getKeyService() { - try { - Method method = Class.forName("android.os.ServiceManager").getMethod("getService", String.class); - IBinder binder = (IBinder) method.invoke(null, "com.lge.ivi.server.Key"); - if (binder != null) { - return IKeyService.Stub.asInterface(binder); - } - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - return null; - } - - public void registerHandler(Handler handler) { - mRegisteredHandlers.add(handler); - } - - public void unregisterHandler(Handler handler) { - mRegisteredHandlers.remove(handler); - } - - public void mapAppToKey(ApplicationInfo applicationInfo) { - mNextAppMappingApplicationInfo = applicationInfo; - } - - public void mapBackKey() { - mMapBackKey = true; - } - - public void clearKeyMapping() { - mClearKeyMapping = true; - } - - public void mapRecentApps() { - mMapRecentApps = true; - } - - public void cancel() { - mNextAppMappingApplicationInfo = null; - mMapBackKey = false; - mClearKeyMapping = false; - mMapRecentApps = false; - } - - public void isActivityVisible(boolean visible) { - mActivityVisible = visible; - } - - private void notifyHandlers(int what, Object obj) { - Message msg = new Message(); - msg.what = what; - msg.obj = obj; - for (Handler handler : mRegisteredHandlers) { - handler.sendMessage(msg); - } - } - - private void injectKeyEvent(int keyCode) { - try { - String keyCommand = "input keyevent " + keyCode + " > /dev/null 2> /dev/null < /dev/null &"; - ProcessExecutor.executeRootCommand(keyCommand); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - - private void openRecentApps() { - try { - Class serviceManagerClass = Class.forName("android.os.ServiceManager"); - Method getService = serviceManagerClass.getMethod("getService", String.class); - IBinder retbinder = (IBinder) getService.invoke(null, "statusbar"); - Class statusBarClass = Class.forName(retbinder.getInterfaceDescriptor()); - Object statusBarObject = statusBarClass.getClasses()[0] - .getMethod("asInterface", IBinder.class).invoke(null, retbinder); - Method toggleRecentApps = statusBarClass.getMethod("toggleRecentApps"); - toggleRecentApps.setAccessible(true); - toggleRecentApps.invoke(statusBarObject); - } catch (Exception e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); - } - } - - public class KeyInterceptorBinder extends Binder { - public KeyInterceptorService getService() { - return KeyInterceptorService.this; - } - } + private void notifyHandlers(int what, Object obj) { + Message msg = new Message(); + msg.what = what; + msg.obj = obj; + for (Handler handler : mRegisteredHandlers) { + handler.sendMessage(msg); + } + } + + public static void injectKeyEvent(int keyCode) { + try { + String keyCommand = "input keyevent " + keyCode + " > /dev/null 2> /dev/null < /dev/null &"; + ProcessExecutor.executeRootCommand(keyCommand); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + private void openRecentApps() { + try { + Class serviceManagerClass = Class.forName("android.os.ServiceManager"); + Method getService = serviceManagerClass.getMethod("getService", String.class); + IBinder retbinder = (IBinder) getService.invoke(null, "statusbar"); + Class statusBarClass = Class.forName(retbinder.getInterfaceDescriptor()); + Object statusBarObject = statusBarClass.getClasses()[0] + .getMethod("asInterface", IBinder.class).invoke(null, retbinder); + Method toggleRecentApps = statusBarClass.getMethod("toggleRecentApps"); + toggleRecentApps.setAccessible(true); + toggleRecentApps.invoke(statusBarObject); + } catch (Exception e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + + public class KeyInterceptorBinder extends Binder { + public KeyInterceptorService getService() { + return KeyInterceptorService.this; + } + } } diff --git a/app/src/main/java/g4rb4g3/at/ctsteststarter/MainActivity.java b/app/src/main/java/g4rb4g3/at/ctsteststarter/MainActivity.java index 6d7f95f..8775fc8 100644 --- a/app/src/main/java/g4rb4g3/at/ctsteststarter/MainActivity.java +++ b/app/src/main/java/g4rb4g3/at/ctsteststarter/MainActivity.java @@ -38,309 +38,318 @@ import static g4rb4g3.at.ctsteststarter.KeyInterceptorService.UNMAPPED_APP; public class MainActivity extends Activity { - private boolean mBound = false; - private KeyInterceptorService mService; - private PackageManager mPackageManager = null; - private ApplicationAdapter mListAdapter = null; - private GridView mGvAppList; - private AlertDialog mAlertDialog; - private boolean mShowAllApps = false; + private static final int CANCEL_DELAY = 2500; + + private boolean mBound = false; + private KeyInterceptorService mService; + private PackageManager mPackageManager = null; + private ApplicationAdapter mListAdapter = null; + private GridView mGvAppList; + private AlertDialog mAlertDialog; + private boolean mShowAllApps = false; + + private final Map contextOptions = new HashMap<>(); + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW_MESSAGE: + Toast.makeText(getApplicationContext(), msg.obj.toString(), Toast.LENGTH_SHORT).show(); + if (mAlertDialog != null && mAlertDialog.isShowing()) { + mAlertDialog.dismiss(); + } + break; + case MAPPED_APP: + mListAdapter.setKeyMapped(msg.obj.toString(), true); + break; + case UNMAPPED_APP: + mListAdapter.setKeyMapped(msg.obj.toString(), false); + break; + } + } + }; - private Map contextOptions = new HashMap<>(); + private final ServiceConnection mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + KeyInterceptorService.KeyInterceptorBinder binder = (KeyInterceptorService.KeyInterceptorBinder) service; + mService = binder.getService(); + mBound = true; + + mService.registerHandler(mHandler); + mService.setActivityVisible(true); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mBound = false; + } + }; + + private final Handler mCancelHandler = new Handler(Looper.getMainLooper()); + private final Runnable mCancelRunnable = () -> { + mService.cancel(); + }; - private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case SHOW_MESSAGE: - Toast.makeText(getApplicationContext(), msg.obj.toString(), Toast.LENGTH_SHORT).show(); - if (mAlertDialog != null && mAlertDialog.isShowing()) { - mAlertDialog.dismiss(); - } - break; - case MAPPED_APP: - mListAdapter.setKeyMapped(msg.obj.toString(), true); - break; - case UNMAPPED_APP: - mListAdapter.setKeyMapped(msg.obj.toString(), false); - break; - } + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + setTitle(getString(R.string.app_name_version, BuildConfig.VERSION_NAME)); + + mPackageManager = getPackageManager(); + + enableDotsMenu(); + + mGvAppList = findViewById(R.id.gv_all_apps); + mGvAppList.setOnItemClickListener((parent, view, position, id) -> { + LaunchableApplicationInfo info = mListAdapter.getItem(position); + showAppOptions(info, position); + }); + + contextOptions.put(getString(R.string.launch), R.string.launch); + contextOptions.put(getString(R.string.clear_cache), R.string.clear_cache); + contextOptions.put(getString(R.string.clear_data), R.string.clear_data); + contextOptions.put(getString(R.string.uninstall), R.string.uninstall); + contextOptions.put(getString(R.string.force_stop), R.string.force_stop); + contextOptions.put(getString(R.string.map_key), R.string.map_key); } - }; - private ServiceConnection mServiceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - KeyInterceptorService.KeyInterceptorBinder binder = (KeyInterceptorService.KeyInterceptorBinder) service; - mService = binder.getService(); - mBound = true; + private void enableDotsMenu() { + try { + ViewConfiguration config = ViewConfiguration.get(this); + Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); - mService.registerHandler(mHandler); - mService.isActivityVisible(true); + if (menuKeyField != null) { + menuKeyField.setAccessible(true); + menuKeyField.setBoolean(config, false); + } + } catch (Exception e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } } @Override - public void onServiceDisconnected(ComponentName name) { - mBound = false; - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - setTitle(getString(R.string.app_name_version, BuildConfig.VERSION_NAME)); - - mPackageManager = getPackageManager(); - - enableDotsMenu(); - - mGvAppList = findViewById(R.id.gv_all_apps); - mGvAppList.setOnItemClickListener((parent, view, position, id) -> { - LaunchableApplicationInfo info = mListAdapter.getItem(position); - showAppOptions(info, position); - }); - - contextOptions.put(getString(R.string.launch), R.string.launch); - contextOptions.put(getString(R.string.clear_cache), R.string.clear_cache); - contextOptions.put(getString(R.string.clear_data), R.string.clear_data); - contextOptions.put(getString(R.string.uninstall), R.string.uninstall); - contextOptions.put(getString(R.string.force_stop), R.string.force_stop); - contextOptions.put(getString(R.string.map_key), R.string.map_key); - } - - private void enableDotsMenu() { - try { - ViewConfiguration config = ViewConfiguration.get(this); - Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); - - if (menuKeyField != null) { - menuKeyField.setAccessible(true); - menuKeyField.setBoolean(config, false); - } - } catch (Exception e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - - @Override - protected void onResume() { - super.onResume(); - bindService(new Intent(this, KeyInterceptorService.class), mServiceConnection, Context.BIND_AUTO_CREATE); - - new LoadApplications().execute(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.main_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.mi_clear_mapping: - mAlertDialog = new AlertDialog.Builder(this) - .setTitle(R.string.next_step) - .setMessage(R.string.long_press_clear_mapping) - .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) - .setCancelable(false) - .create(); - mAlertDialog.show(); - mService.clearKeyMapping(); - break; - case R.id.mi_map_back_key: - mAlertDialog = new AlertDialog.Builder(this) - .setTitle(R.string.next_step) - .setMessage(R.string.long_press_map_back) - .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) - .setCancelable(false) - .create(); - mAlertDialog.show(); - mService.mapBackKey(); - break; - case R.id.mi_show_all_apps: - item.setChecked(!item.isChecked()); - mShowAllApps = item.isChecked(); + protected void onResume() { + super.onResume(); + mCancelHandler.removeCallbacks(mCancelRunnable); + bindService(new Intent(this, KeyInterceptorService.class), mServiceConnection, Context.BIND_AUTO_CREATE); new LoadApplications().execute(); - break; - case R.id.mi_map_recents: - mAlertDialog = new AlertDialog.Builder(this) - .setTitle(R.string.next_step) - .setMessage(R.string.long_press_map_recent) - .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) - .setCancelable(false) - .create(); - mAlertDialog.show(); - mService.mapRecentApps(); - break; - } - return true; - } - - @Override - protected void onPause() { - super.onPause(); - if (mBound) { - mService.isActivityVisible(false); - mService.cancel(); - mService.unregisterHandler(mHandler); - unbindService(mServiceConnection); } - if (mAlertDialog != null && mAlertDialog.isShowing()) { - mAlertDialog.dismiss(); - } - } - public void mapAppToKey(final LaunchableApplicationInfo info) { - if (!info.isLaunchable) { - Toast.makeText(this, R.string.not_assignable, Toast.LENGTH_SHORT).show(); - return; - } - mAlertDialog = new AlertDialog.Builder(this) - .setTitle(R.string.next_step) - .setMessage(getString(R.string.long_press_to_map_app, info.name)) - .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) - .setCancelable(false) - .create(); - mAlertDialog.show(); - mService.mapAppToKey(info); - } - - private void showAppOptions(final LaunchableApplicationInfo info, final int position) { - final ArrayAdapter items = new ArrayAdapter<>(this, android.R.layout.simple_selectable_list_item); - if (info.isLaunchable) { - if (!info.isKeyMapped) { - items.add(getString(R.string.map_key)); - } - items.add(getString(R.string.launch)); - } - items.addAll(getString(R.string.clear_cache), getString(R.string.clear_data), getString(R.string.force_stop)); - if (!info.isSystemApp) { - items.add(getString(R.string.uninstall)); - } - new AlertDialog.Builder(this) - .setTitle(info.name + " " + info.version) - .setIcon(info.loadIcon(mPackageManager)) - .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) - .setAdapter(items, (dialog, which) -> { - switch (contextOptions.get(items.getItem(which))) { - case R.string.launch: - startActivity(mPackageManager.getLaunchIntentForPackage(info.packageName)); - break; - case R.string.uninstall: - try { - ProcessExecutor.executeRootCommand("pm uninstall " + info.packageName); - mListAdapter.remove(info); - mListAdapter.notifyDataSetChanged(); - - SharedPreferences sharedPreferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); - Map allEntries = sharedPreferences.getAll(); - for (Map.Entry entry : allEntries.entrySet()) { - if (info.packageName.equals(entry.getValue().toString())) { - sharedPreferences.edit().remove(entry.getKey()).commit(); - break; - } - } - } catch (RemoteException e) { - Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); - } - break; - case R.string.clear_cache: - try { - ProcessExecutor.executeRootCommand("rm -rf /data/data/" + info.packageName + "/cache/*"); - } catch (RemoteException e) { - Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); - } - break; - case R.string.clear_data: - try { - killAppIfRunning(info.packageName); - ProcessExecutor.executeRootCommand("rm -rf /data/data/" + info.packageName + "/*"); - } catch (RemoteException e) { - Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); - } - break; - case R.string.force_stop: - try { - killAppIfRunning(info.packageName); - } catch (RemoteException e) { - Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); - } - break; - case R.string.map_key: - mapAppToKey(info); - break; - } - }) - .show(); - } - - private void killAppIfRunning(String packageName) throws RemoteException { - String pid = ProcessExecutor.execute("/system/bin/sh", "-c", "ps | grep " + packageName + " | busybox awk '{print $2}'"); - if (pid != null) { - ProcessExecutor.executeRootCommand("kill " + pid); + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return true; } - } - - private class LoadApplications extends AsyncTask { - private ProgressDialog progress = null; @Override - protected Void doInBackground(Void... params) { - List appList = checkForLaunchIntent(mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA), MainActivity.this); - mListAdapter = new ApplicationAdapter(MainActivity.this, R.layout.app_list_item, appList); - return null; + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.mi_clear_mapping: + mAlertDialog = new AlertDialog.Builder(this) + .setTitle(R.string.next_step) + .setMessage(R.string.press_clear_mapping) + .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) + .setCancelable(false) + .create(); + mAlertDialog.show(); + mService.clearKeyMapping(); + break; + case R.id.mi_map_back_key: + mAlertDialog = new AlertDialog.Builder(this) + .setTitle(R.string.next_step) + .setMessage(R.string.press_map_back) + .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) + .setCancelable(false) + .create(); + mAlertDialog.show(); + mService.mapBackKey(); + break; + case R.id.mi_show_all_apps: + item.setChecked(!item.isChecked()); + mShowAllApps = item.isChecked(); + new LoadApplications().execute(); + break; + case R.id.mi_map_recents: + mAlertDialog = new AlertDialog.Builder(this) + .setTitle(R.string.next_step) + .setMessage(R.string.press_map_recent) + .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) + .setCancelable(false) + .create(); + mAlertDialog.show(); + mService.mapRecentApps(); + break; + } + return true; } @Override - protected void onPostExecute(Void result) { - mGvAppList.setAdapter(mListAdapter); - progress.dismiss(); - super.onPostExecute(result); + protected void onPause() { + super.onPause(); + + mCancelHandler.postDelayed(mCancelRunnable, CANCEL_DELAY); + + if (mBound) { + mService.setActivityVisible(false); + mService.unregisterHandler(mHandler); + unbindService(mServiceConnection); + } + if (mAlertDialog != null && mAlertDialog.isShowing()) { + mAlertDialog.dismiss(); + } } - @Override - protected void onPreExecute() { - progress = ProgressDialog.show(MainActivity.this, null, getString(R.string.loading_applist)); - super.onPreExecute(); + public void mapAppToKey(final LaunchableApplicationInfo info) { + if (!info.isLaunchable) { + Toast.makeText(this, R.string.not_assignable, Toast.LENGTH_SHORT).show(); + return; + } + mAlertDialog = new AlertDialog.Builder(this) + .setTitle(R.string.next_step) + .setMessage(getString(R.string.press_to_map_app, info.name)) + .setNegativeButton(R.string.cancel, (dialog, which) -> mService.cancel()) + .setCancelable(false) + .create(); + mAlertDialog.show(); + mService.mapAppToKey(info); } - @Override - protected void onProgressUpdate(Void... values) { - super.onProgressUpdate(values); + private void showAppOptions(final LaunchableApplicationInfo info, final int position) { + final ArrayAdapter items = new ArrayAdapter<>(this, android.R.layout.simple_selectable_list_item); + if (info.isLaunchable) { + if (!info.isKeyMapped) { + items.add(getString(R.string.map_key)); + } + items.add(getString(R.string.launch)); + } + items.addAll(getString(R.string.clear_cache), getString(R.string.clear_data), getString(R.string.force_stop)); + if (!info.isSystemApp) { + items.add(getString(R.string.uninstall)); + } + new AlertDialog.Builder(this) + .setTitle(info.name + " " + info.version) + .setIcon(info.loadIcon(mPackageManager)) + .setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()) + .setAdapter(items, (dialog, which) -> { + switch (contextOptions.get(items.getItem(which))) { + case R.string.launch: + startActivity(mPackageManager.getLaunchIntentForPackage(info.packageName)); + break; + case R.string.uninstall: + try { + ProcessExecutor.executeRootCommand("pm uninstall " + info.packageName); + mListAdapter.remove(info); + mListAdapter.notifyDataSetChanged(); + + SharedPreferences sharedPreferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE); + Map allEntries = sharedPreferences.getAll(); + for (Map.Entry entry : allEntries.entrySet()) { + if (info.packageName.equals(entry.getValue().toString())) { + sharedPreferences.edit().remove(entry.getKey()).commit(); + break; + } + } + } catch (RemoteException e) { + Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); + } + break; + case R.string.clear_cache: + try { + ProcessExecutor.executeRootCommand("rm -rf /data/data/" + info.packageName + "/cache/*"); + } catch (RemoteException e) { + Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); + } + break; + case R.string.clear_data: + try { + killAppIfRunning(info.packageName); + ProcessExecutor.executeRootCommand("rm -rf /data/data/" + info.packageName + "/*"); + } catch (RemoteException e) { + Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); + } + break; + case R.string.force_stop: + try { + killAppIfRunning(info.packageName); + } catch (RemoteException e) { + Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); + } + break; + case R.string.map_key: + mapAppToKey(info); + break; + } + }) + .show(); } - private List checkForLaunchIntent(List list, Context context) { - List mappedApps = new ArrayList<>(); - Map preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).getAll(); - for (Map.Entry entry : preferences.entrySet()) { - if (TextUtils.isDigitsOnly(entry.getKey())) { - mappedApps.add(entry.getValue().toString()); + private void killAppIfRunning(String packageName) throws RemoteException { + String pid = ProcessExecutor.execute("/system/bin/sh", "-c", "ps | grep " + packageName + " | busybox awk '{print $2}'"); + if (pid != null) { + ProcessExecutor.executeRootCommand("kill " + pid); } - } - String ownPackageName = getPackageName(); - ArrayList applist = new ArrayList<>(); - for (ApplicationInfo info : list) { - try { - if (!ownPackageName.equals(info.packageName)) { - LaunchableApplicationInfo applicationInfo = new LaunchableApplicationInfo(info, mPackageManager.getLaunchIntentForPackage(info.packageName) != null, context); - if (!mShowAllApps && !applicationInfo.isLaunchable) { - continue; - } - applicationInfo.isKeyMapped = mappedApps.contains(applicationInfo.packageName); - String appLabel = applicationInfo.loadLabel(mPackageManager).toString(); - applicationInfo.name = appLabel == null ? "" : appLabel; - applist.add(applicationInfo); - } - } catch (Exception e) { - e.printStackTrace(); + } + + private class LoadApplications extends AsyncTask { + private ProgressDialog progress = null; + + @Override + protected Void doInBackground(Void... params) { + List appList = checkForLaunchIntent(mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA), MainActivity.this); + mListAdapter = new ApplicationAdapter(MainActivity.this, R.layout.app_list_item, appList); + return null; + } + + @Override + protected void onPostExecute(Void result) { + mGvAppList.setAdapter(mListAdapter); + progress.dismiss(); + super.onPostExecute(result); + } + + @Override + protected void onPreExecute() { + progress = ProgressDialog.show(MainActivity.this, null, getString(R.string.loading_applist)); + super.onPreExecute(); } - } - Collections.sort(applist, (o1, o2) -> o1.name.toLowerCase().compareTo(o2.name.toLowerCase())); - return applist; + @Override + protected void onProgressUpdate(Void... values) { + super.onProgressUpdate(values); + } + + private List checkForLaunchIntent(List list, Context context) { + List mappedApps = new ArrayList<>(); + Map preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE).getAll(); + for (Map.Entry entry : preferences.entrySet()) { + if (TextUtils.isDigitsOnly(entry.getKey()) || entry.getKey().endsWith("_dbl")) { + mappedApps.add(entry.getValue().toString()); + } + } + String ownPackageName = getPackageName(); + ArrayList applist = new ArrayList<>(); + for (ApplicationInfo info : list) { + try { + if (!ownPackageName.equals(info.packageName)) { + LaunchableApplicationInfo applicationInfo = new LaunchableApplicationInfo(info, mPackageManager.getLaunchIntentForPackage(info.packageName) != null, context); + if (!mShowAllApps && !applicationInfo.isLaunchable) { + continue; + } + applicationInfo.isKeyMapped = mappedApps.contains(applicationInfo.packageName); + String appLabel = applicationInfo.loadLabel(mPackageManager).toString(); + applicationInfo.name = appLabel == null ? "" : appLabel; + applist.add(applicationInfo); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + Collections.sort(applist, (o1, o2) -> o1.name.toLowerCase().compareTo(o2.name.toLowerCase())); + return applist; + } } - } } diff --git a/app/src/main/java/g4rb4g3/at/ctsteststarter/ServiceRestartBroadcastReceiver.java b/app/src/main/java/g4rb4g3/at/ctsteststarter/ServiceRestartBroadcastReceiver.java new file mode 100644 index 0000000..a3b922e --- /dev/null +++ b/app/src/main/java/g4rb4g3/at/ctsteststarter/ServiceRestartBroadcastReceiver.java @@ -0,0 +1,14 @@ +package g4rb4g3.at.ctsteststarter; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.widget.Toast; + +public class ServiceRestartBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + context.startService(new Intent(context, KeyInterceptorService.class)); + Toast.makeText(context, "AppManager service restarted.", Toast.LENGTH_SHORT).show(); + } +} \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000..4ce88bd --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,30 @@ + + App Manager + App Manager %1$s + Hosszú vagy dupla billentyű hozzárendelés + Hozzárendelés törlése + Vissza gomb hozzárenelése + Hogyan + OK + Mégsem + Hosszú vagy dupla nyomás a(z) \"%1$s\" alkalmazáshoz rendeléshez. + Hosszú vagy dupla nyomás a hozzárendelés törléséhez. + Hosszú vagy dupla nyomás a "vissza gomb" hozzárendeléséhez. + Hosszú vagy dupla nyomás a "legutóbbi alkalmazások" menü hozzárendeléséhez. + Alkalmazás adatok betöltése… + Következő lépés + A %1$s billentyűkód hozzárendelve a %2$s alkalmazáshoz. + A hozzárendelés a %1$s apphoz megszakítva." + A hozzárendelés a %1$s billentyűkódhoz törölve. + A %1$s hozzárendelve a vissza gombhoz. + A %1$s hozzárendelve a "legutóbbi alkalmazások" menühöz. + A beállítások gomb nem hozzárendelhető. + Indítás + Eltávolítás + Az összes alkalmazás mutatása + Az alkalmazás nem hozzárendelhető, mert nincs futtatható célja. + Gyorsítótár törlése + Adatok törlése + Kényszerített leállítás + "Legutóbbi alkalmazások" hozzárendelése + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9645ce..1561b9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,23 +1,23 @@ App Manager App Manager %1$s - Map key + Map key for long or double keypress Clear mapping Map back key How-To - Ok + OK Cancel - Long press key to map application \"%1$s\". - Long press key to clear mapping. - Long press key to map back key. - Long press key to map recent apps menu. - Loading application info... + Long or double press key to map application \"%1$s\". + Long press or double press key to clear mapping. + Long press or double press key to map back key. + Long press or double press key to map recent apps menu. + Loading application info… Next step - Mapped keycode %1$d to application %2$s + Mapped keycode %1$s to application %2$s Mapping app %1$s aborted." - Mapping for keycode %1$d cleared. - Mapped keycode %1$d on back key. - Mapped keycode %1$d to recent apps menu. + Mapping for keycode %1$s cleared. + Mapped keycode %1$s on back key. + Mapped keycode %1$s to recent apps menu. Settings button can\'t be mapped. Launch Uninstall diff --git a/build.gradle b/build.gradle index baa4ebb..d91fd7e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0-beta02' + classpath 'com.android.tools.build:gradle:7.0.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 85b2ceb..46b79d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip