diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4355842d..54ed9803 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,6 @@ android:theme="@style/Theme.ActivityLauncher"> - @@ -31,21 +30,14 @@ android:name=".SettingsActivity" android:label="@string/activity_settings" android:exported="false" /> - - - - - + diff --git a/app/src/main/java/de/szalkowski/activitylauncher/ShortcutActivity.kt b/app/src/main/java/de/szalkowski/activitylauncher/ShortcutActivity.kt index 45661bda..d834ca8d 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/ShortcutActivity.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/ShortcutActivity.kt @@ -3,27 +3,41 @@ package de.szalkowski.activitylauncher import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat import dagger.hilt.android.AndroidEntryPoint import de.szalkowski.activitylauncher.services.ActivityLauncherService +import de.szalkowski.activitylauncher.services.IconCreatorService +import de.szalkowski.activitylauncher.services.IntentSigningService import javax.inject.Inject @AndroidEntryPoint class ShortcutActivity : AppCompatActivity() { @Inject - internal lateinit var activityLauncherService: ActivityLauncherService + internal lateinit var launcherService: ActivityLauncherService + + @Inject + internal lateinit var signingService: IntentSigningService + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) try { - val launchIntent = Intent.parseUri(intent.getStringExtra("extra_intent"), 0) - activityLauncherService.launchActivity(launchIntent.component!!, - asRoot = false, + val launchIntent = Intent.parseUri(intent.getStringExtra(IconCreatorService.INTENT_EXTRA_INTENT), 0) + val signature = intent.getStringExtra(IconCreatorService.INTENT_EXTRA_SIGNATURE).orEmpty() + val asRoot = intent.action == IconCreatorService.INTENT_LAUNCH_ROOT_SHORTCUT + + if (asRoot && !signingService.validateIntentSignature(launchIntent, signature)) { + return + } + + launcherService.launchActivity(launchIntent.component!!, + asRoot, showToast = false - ); + ) } catch (e: Exception) { e.printStackTrace() } finally { - finish() + ActivityCompat.finishAffinity(this); } } } diff --git a/app/src/main/java/de/szalkowski/activitylauncher/services/ActivityLauncherService.kt b/app/src/main/java/de/szalkowski/activitylauncher/services/ActivityLauncherService.kt index 8a7d17cb..a140be0b 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/services/ActivityLauncherService.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/services/ActivityLauncherService.kt @@ -34,7 +34,7 @@ class ActivityLauncherServiceImpl @Inject constructor(@ActivityContext private v asRoot: Boolean, showToast: Boolean ) { - val intent: Intent = getActivityIntent(activity, null) + val intent = getActivityIntent(activity, null) if (showToast) Toast.makeText( context, String.format( @@ -69,6 +69,7 @@ class ActivityLauncherServiceImpl @Inject constructor(@ActivityContext private v component ) } + val process = Runtime.getRuntime().exec( arrayOf( "su", "-c", diff --git a/app/src/main/java/de/szalkowski/activitylauncher/services/Bindings.kt b/app/src/main/java/de/szalkowski/activitylauncher/services/Bindings.kt index 61f6db4c..8938d328 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/services/Bindings.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/services/Bindings.kt @@ -34,6 +34,12 @@ abstract class ServicesModule { @Module @InstallIn(SingletonComponent::class) abstract class ApplicationServicesModule { + @Singleton + @Binds + abstract fun bindIntentSigningService( + intentSigningServiceImpl: IntentSigningServiceImpl + ): IntentSigningService + @Singleton @Binds abstract fun bindRootDetectionService( diff --git a/app/src/main/java/de/szalkowski/activitylauncher/services/IconCreatorService.kt b/app/src/main/java/de/szalkowski/activitylauncher/services/IconCreatorService.kt index 6fb78d34..a918e20a 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/services/IconCreatorService.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/services/IconCreatorService.kt @@ -21,42 +21,45 @@ import androidx.appcompat.app.AlertDialog import dagger.hilt.android.qualifiers.ActivityContext import de.szalkowski.activitylauncher.R import de.szalkowski.activitylauncher.services.internal.getActivityIntent -import java.util.Objects import javax.inject.Inject -private const val INTENT_LAUNCH_SHORTCUT = "activitylauncher.intent.action.LAUNCH_SHORTCUT" - interface IconCreatorService { - fun createLauncherIcon(activity: MyActivityInfo, extras: Bundle?) - fun createLauncherIcon(activity: MyActivityInfo) - fun createLauncherIcon(pack: MyPackageInfo) + fun createLauncherIcon(activity: MyActivityInfo, optionalExtras: Bundle? = null) + fun createRootLauncherIcon(activity: MyActivityInfo, optionalExtras: Bundle? = null) + + companion object { + const val INTENT_LAUNCH_SHORTCUT = "activitylauncher.intent.action.LAUNCH_SHORTCUT" + const val INTENT_LAUNCH_ROOT_SHORTCUT = "activitylauncher.intent.action.LAUNCH_ROOT_SHORTCUT" + + const val INTENT_EXTRA_INTENT = "extra_intent" + const val INTENT_EXTRA_SIGNATURE = "sign" + } } -class IconCreatorServiceImpl @Inject constructor(@ActivityContext private val context: Context) : - IconCreatorService { - override fun createLauncherIcon(activity: MyActivityInfo, extras: Bundle?) { +class IconCreatorServiceImpl @Inject constructor( + @ActivityContext private val context: Context, + private val signingService: IntentSigningService +) : IconCreatorService { + override fun createLauncherIcon(activity: MyActivityInfo, optionalExtras: Bundle?) { + createLauncherIcon(activity, optionalExtras, false) + } + + override fun createRootLauncherIcon(activity: MyActivityInfo, optionalExtras: Bundle?) { + createLauncherIcon(activity, optionalExtras, true) + } + + private fun createLauncherIcon(activity: MyActivityInfo, optionalExtras: Bundle?, asRoot: Boolean) { val pack = extractIconPackageName(activity) - val name: String = activity.name - val intent = getActivityIntent(activity.componentName, extras) - val icon: Drawable = activity.icon + val intent = getActivityIntent(activity.componentName, optionalExtras) // Use bitmap version, if icon from different package is used if (pack != null && pack != activity.componentName.packageName) { - createShortcut(name, icon, intent, null) + createShortcut(activity.name, intent, activity.icon, asRoot, null) } else { - createShortcut(name, icon, intent, activity.iconResourceName) + createShortcut(activity.name, intent, activity.icon, asRoot, activity.iconResourceName) } } - override fun createLauncherIcon(activity: MyActivityInfo) { - createLauncherIcon(activity, null) - } - - override fun createLauncherIcon(pack: MyPackageInfo) { - val intent = context.packageManager.getLaunchIntentForPackage(pack.packageName) ?: return - createShortcut(pack.name, pack.icon, intent, pack.iconResourceName) - } - private fun extractIconPackageName( activity: MyActivityInfo, ): String? { @@ -105,7 +108,7 @@ class IconCreatorServiceImpl @Inject constructor(@ActivityContext private val co } private fun createShortcut( - appName: String, draw: Drawable, intent: Intent, iconResourceName: String? + appName: String, intent: Intent, draw: Drawable, asRoot: Boolean, iconResourceName: String? ) { Toast.makeText( context, String.format( @@ -113,17 +116,22 @@ class IconCreatorServiceImpl @Inject constructor(@ActivityContext private val co ), Toast.LENGTH_LONG ).show() if (Build.VERSION.SDK_INT >= 26) { - doCreateShortcut(appName, draw, intent) + doCreateShortcut(appName, intent, asRoot, draw) } else { - doCreateShortcut(appName, intent, iconResourceName) + doCreateShortcut(appName, intent, asRoot, iconResourceName) } } private fun doCreateShortcut( - appName: String, intent: Intent, iconResourceName: String? + appName: String, intent: Intent, asRoot: Boolean, iconResourceName: String? ) { val shortcutIntent = Intent() - shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent) + if (asRoot) { + // wrap only if root access needed + shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, createShortcutIntent(intent, true)) + } else { + shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent) + } shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, appName) if (iconResourceName != null) { val ir = ShortcutIconResource() @@ -134,6 +142,7 @@ class IconCreatorServiceImpl @Inject constructor(@ActivityContext private val co } ir.resourceName = iconResourceName shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, ir) + } shortcutIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT") context.sendBroadcast(shortcutIntent) @@ -141,20 +150,15 @@ class IconCreatorServiceImpl @Inject constructor(@ActivityContext private val co @TargetApi(26) private fun doCreateShortcut( - appName: String, draw: Drawable, extraIntent: Intent + appName: String, intent: Intent, asRoot: Boolean, draw: Drawable ) { - val shortcutManager = Objects.requireNonNull( - context.getSystemService( - ShortcutManager::class.java - ) - ) + val shortcutManager = context.getSystemService(ShortcutManager::class.java)!! if (shortcutManager.isRequestPinShortcutSupported) { val icon = getIconFromDrawable(draw) - val intent = Intent(INTENT_LAUNCH_SHORTCUT) - intent.putExtra("extra_intent", extraIntent.toUri(0)) + val shortcutIntent = createShortcutIntent(intent, asRoot) val shortcutInfo = ShortcutInfo.Builder(context, appName).setShortLabel(appName).setLongLabel(appName) - .setIcon(icon).setIntent(intent).build() + .setIcon(icon).setIntent(shortcutIntent).build() shortcutManager.requestPinShortcut(shortcutInfo, null) } else { AlertDialog.Builder(context).setTitle(context.getText(R.string.error_creating_shortcut)) @@ -166,5 +170,25 @@ class IconCreatorServiceImpl @Inject constructor(@ActivityContext private val co }.show() } } + + private fun createShortcutIntent(intent: Intent, asRoot: Boolean): Intent { + val action = if(asRoot) { + IconCreatorService.INTENT_LAUNCH_ROOT_SHORTCUT} else {IconCreatorService.INTENT_LAUNCH_SHORTCUT} + val shortcutIntent = Intent(action) + shortcutIntent.putExtra(IconCreatorService.INTENT_EXTRA_INTENT, intent.toUri(0)) + + val signature: String + try { + signature = signingService.signIntent(intent) + shortcutIntent.putExtra(IconCreatorService.INTENT_EXTRA_SIGNATURE, signature) + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText( + context, context.getText(R.string.error).toString() + ": " + e, Toast.LENGTH_LONG + ).show() + } + + return shortcutIntent + } } diff --git a/app/src/main/java/de/szalkowski/activitylauncher/services/IntentSigningService.kt b/app/src/main/java/de/szalkowski/activitylauncher/services/IntentSigningService.kt index 9239e853..4070a4a3 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/services/IntentSigningService.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/services/IntentSigningService.kt @@ -1,52 +1,59 @@ -package de.szalkowski.activitylauncher.todo; - -import android.content.ComponentName; -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Base64; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; +package de.szalkowski.activitylauncher.services; + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.util.Base64 +import dagger.hilt.android.qualifiers.ApplicationContext +import java.security.SecureRandom +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject + + +interface IntentSigningService { + fun signIntent(intent: Intent): String + fun validateIntentSignature(intent: Intent, signature: String): Boolean +} -public class Signer { - private final String key; +class IntentSigningServiceImpl @Inject constructor(@ApplicationContext context: Context) : + IntentSigningService { + private val key: String - public Signer(Context context) { - SharedPreferences preferences = context.getSharedPreferences("signer", Context.MODE_PRIVATE); + init { + val preferences = context.getSharedPreferences("signer", Context.MODE_PRIVATE) if (!preferences.contains("key")) { - SecureRandom random = new SecureRandom(); - byte[] bytes = new byte[256]; - random.nextBytes(bytes); - - this.key = Base64.encodeToString(bytes, Base64.NO_WRAP); - preferences.edit().putString("key", this.key).apply(); + val random = SecureRandom() + val bytes = ByteArray(256) + random.nextBytes(bytes) + key = Base64.encodeToString(bytes, Base64.NO_WRAP) + preferences.edit().putString("key", key).apply() } else { - this.key = preferences.getString("key", ""); + key = preferences.getString("key", "")!! } } - /** - * Adapted from StackOverflow: - * https://stackoverflow.com/questions/36004761/is-there-any-function-for-creating-hmac256-string-in-android - */ - private static String hmac256(String key, String message) throws NoSuchAlgorithmException, InvalidKeyException { - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(key.getBytes(), "HmacSHA256")); - byte[] result = mac.doFinal(message.getBytes()); - return Base64.encodeToString(result, Base64.NO_WRAP); + override fun signIntent(intent: Intent): String { + val uri = intent.toUri(0) + return hmac256(key, uri) } - public String signComponentName(ComponentName comp) throws InvalidKeyException, NoSuchAlgorithmException { - String name = comp.flattenToShortString(); - return hmac256(this.key, name); + override fun validateIntentSignature(intent: Intent, signature: String): Boolean { + val compSignature = signIntent(intent) + return signature == compSignature } - public boolean validateComponentNameSignature(ComponentName comp, String signature) throws InvalidKeyException, NoSuchAlgorithmException { - String compSignature = this.signComponentName(comp); - return signature.equals(compSignature); + companion object { + /** + * Adapted from StackOverflow: + * https://stackoverflow.com/questions/36004761/is-there-any-function-for-creating-hmac256-string-in-android + */ + private fun hmac256(key: String?, message: String): String { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(key!!.toByteArray(), "HmacSHA256")) + val result = mac.doFinal(message.toByteArray()) + return Base64.encodeToString(result, Base64.NO_WRAP) + } } } + diff --git a/app/src/main/java/de/szalkowski/activitylauncher/services/internal/ActivityIntent.kt b/app/src/main/java/de/szalkowski/activitylauncher/services/internal/ActivityIntent.kt index b491e1e0..64522ed8 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/services/internal/ActivityIntent.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/services/internal/ActivityIntent.kt @@ -7,8 +7,7 @@ import android.os.Bundle fun getActivityIntent(activity: ComponentName?, extras: Bundle?): Intent { val intent = Intent() intent.setComponent(activity) - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK if (extras != null) { intent.putExtras(extras) } diff --git a/app/src/main/java/de/szalkowski/activitylauncher/ui/ActivityDetailsFragment.kt b/app/src/main/java/de/szalkowski/activitylauncher/ui/ActivityDetailsFragment.kt index 2ca882f1..f4446238 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/ui/ActivityDetailsFragment.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/ui/ActivityDetailsFragment.kt @@ -63,11 +63,21 @@ class ActivityDetailsFragment : Fragment() { iconCreatorService.createLauncherIcon(editedActivityInfo) } + binding.btCreateShortcutAsRoot.setOnClickListener { + iconCreatorService.createRootLauncherIcon(editedActivityInfo) + } + binding.btLaunch.setOnClickListener { activityLauncherService.launchActivity( editedActivityInfo.componentName, asRoot = false, showToast = true ) } + + binding.btLaunchAsRoot.setOnClickListener { + activityLauncherService.launchActivity( + editedActivityInfo.componentName, asRoot = true, showToast = true + ) + } } override fun onDestroyView() { diff --git a/app/src/main/java/de/szalkowski/activitylauncher/ui/PackageListFragment.kt b/app/src/main/java/de/szalkowski/activitylauncher/ui/PackageListFragment.kt index 28105e2e..0633e92b 100644 --- a/app/src/main/java/de/szalkowski/activitylauncher/ui/PackageListFragment.kt +++ b/app/src/main/java/de/szalkowski/activitylauncher/ui/PackageListFragment.kt @@ -38,6 +38,7 @@ class PackageListFragment : Fragment() { } binding.rvPackages.adapter = packageListAdapter binding.rvPackages.layoutManager = LinearLayoutManager(requireContext()) + binding.rvPackages.isNestedScrollingEnabled = false } override fun onDestroyView() {