Skip to content

Commit

Permalink
feat: port root launcher
Browse files Browse the repository at this point in the history
  • Loading branch information
butzist committed Feb 17, 2024
1 parent 2d88602 commit f7772c0
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 95 deletions.
12 changes: 2 additions & 10 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,21 @@
android:theme="@style/Theme.ActivityLauncher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/activity_settings"
android:exported="false" />
<activity
android:name=".todo.RootLauncherActivity"
android:label="@string/context_action_launch_as_root"
android:exported="true"
android:theme="@style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<activity
android:name=".ShortcutActivity"
android:exported="true"
android:noHistory="true"
android:theme="@style/Theme.NoDisplay">
<intent-filter>
<action android:name="activitylauncher.intent.action.LAUNCH_SHORTCUT"/>
<action android:name="activitylauncher.intent.action.LAUNCH_ROOT_SHORTCUT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -69,6 +69,7 @@ class ActivityLauncherServiceImpl @Inject constructor(@ActivityContext private v
component
)
}

val process = Runtime.getRuntime().exec(
arrayOf(
"su", "-c",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down Expand Up @@ -105,25 +108,30 @@ 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(
context.getText(R.string.creating_application_shortcut).toString(), appName
), 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()
Expand All @@ -134,27 +142,23 @@ 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)
}

@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))
Expand All @@ -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
}
}

Original file line number Diff line number Diff line change
@@ -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)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit f7772c0

Please sign in to comment.