├── .gitignore ├── .idea ├── .gitignore ├── .name ├── AndroidProjectSystem.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── icon.png ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── PRIVACY.md ├── README.md ├── app ├── .gitignore ├── FeatureImage.afdesign ├── FeatureImage.png ├── LauncherBackground.afphoto ├── LauncherBackground.png ├── LauncherBackground.svg ├── LauncherForeground.afdesign ├── LauncherForeground.svg ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── tk │ │ └── zwander │ │ └── rootactivitylauncher │ │ ├── App.kt │ │ ├── MainActivity.kt │ │ ├── activities │ │ ├── ShortcutLaunchActivity.kt │ │ └── tasker │ │ │ └── TaskerLaunchComponentConfigureActivity.kt │ │ ├── data │ │ ├── BaseOption.kt │ │ ├── ComponentActionButton.kt │ │ ├── ExtraInfo.kt │ │ ├── FilterMode.kt │ │ ├── PrefManager.kt │ │ ├── Query.kt │ │ ├── SortMode.kt │ │ ├── SortOrder.kt │ │ ├── component │ │ │ ├── ActivityInfo.kt │ │ │ ├── Availability.kt │ │ │ ├── BaseComponentInfo.kt │ │ │ ├── ComponentType.kt │ │ │ ├── ReceiverInfo.kt │ │ │ └── ServiceInfo.kt │ │ ├── model │ │ │ ├── AppModel.kt │ │ │ ├── BaseInfoModel.kt │ │ │ ├── ExtrasDialogModel.kt │ │ │ ├── FavoriteModel.kt │ │ │ └── MainModel.kt │ │ └── tasker │ │ │ └── TaskerLaunchComponentInfo.kt │ │ ├── util │ │ ├── AdvancedSearcher.kt │ │ ├── CollectionUtils.kt │ │ ├── ComponentInfoUtils.kt │ │ ├── ComponentUtils.kt │ │ ├── CompositionLocals.kt │ │ ├── DhizukuUtils.kt │ │ ├── EnabledUtils.kt │ │ ├── InfoGetter.kt │ │ ├── PackageUtils.kt │ │ ├── PermissionResultListener.kt │ │ ├── PermissionUtils.kt │ │ ├── UIUtils.kt │ │ ├── Utils.kt │ │ ├── launch │ │ │ ├── LaunchArgs.kt │ │ │ ├── LaunchStrategy.kt │ │ │ ├── LaunchStrategyRegistry.kt │ │ │ └── LaunchUtils.kt │ │ ├── receiver │ │ │ ├── AdminReceiver.kt │ │ │ └── KnoxLicenseReceiver.kt │ │ └── tasker │ │ │ ├── TaskerLaunchComponentHelper.kt │ │ │ └── TaskerLaunchComponentRunner.kt │ │ └── views │ │ ├── MainView.kt │ │ ├── ScrimView.kt │ │ ├── components │ │ ├── AppItem.kt │ │ ├── AppList.kt │ │ ├── BottomBar.kt │ │ ├── ComponentBar.kt │ │ ├── ComponentGroup.kt │ │ ├── ComponentItem.kt │ │ ├── Menu.kt │ │ ├── OptionGroup.kt │ │ ├── SearchComponent.kt │ │ └── SelectableCard.kt │ │ ├── dialogs │ │ ├── AdvancedUsageDialog.kt │ │ ├── BaseAlertDialog.kt │ │ ├── ComponentInfoDialog.kt │ │ ├── ExtraTypeDialog.kt │ │ ├── ExtrasDialog.kt │ │ ├── FilterDialog.kt │ │ ├── PatreonSupportersDialog.kt │ │ └── SortDialog.kt │ │ └── theme │ │ └── Theme.kt │ └── res │ ├── drawable │ ├── baseline_favorite_24.xml │ ├── ic_baseline_filter_list_24.xml │ ├── ic_baseline_help_outline_24.xml │ ├── ic_baseline_link_24.xml │ ├── ic_baseline_open_in_new_24.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── save.xml │ ├── scroll_to_bottom.xml │ ├── scroll_to_top.xml │ ├── sort.xml │ └── tune.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-v31 │ └── colors.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── device_admin.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | /deploymentTargetSelector.xml 5 | /other.xml 6 | /codeStyles/Project.xml 7 | /appInsightsSettings.xml 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Root Activity Launcher -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/.idea/icon.png -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 59 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | 30 | 31 | 54 | 74 | 75 | 76 | 77 | 78 | 79 | 81 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Root Activity Launcher collects only the data needed for crash logs, along with the [default Firebase analytics events](https://support.google.com/firebase/answer/9234069?hl=en). 4 | 5 | No personally identifying information is collected, and no data is shared beyond the scope of Firebase. 6 | 7 | ## Permissions 8 | 9 | - com.android.launcher.permission.INSTALL_SHORTCUT 10 | - Used to allow Root Activity Launcher to add shortcuts to the home screen. 11 | - android.permission.QUERY_ALL_PACKAGES 12 | - Used to allow Root Activity Launcher to display all installed apps. 13 | - android.permission.FOREGROUND_SERVICE 14 | - Used to allow Root Activity Launcher to start app Services in the foreground. 15 | - moe.shizuku.manager.permission.API_V23 16 | - Used to allow Root Activity Launcher to use [Shizuku](https://shizuku.rikka.app) to access privileged APIs. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Root Activity Launcher 2 | A utility app to launch and modify app components with potentially privileged access. 3 | 4 | ## Download 5 | https://play.google.com/store/apps/details?id=tk.zwander.rootactivitylauncher 6 | 7 | ## Error Reporting 8 | Root Activity Launcher uses Bugsnag for error reporting as of version 28. Previous versions use Firebase Crashlytics. 9 | 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /app/FeatureImage.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/FeatureImage.afdesign -------------------------------------------------------------------------------- /app/FeatureImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/FeatureImage.png -------------------------------------------------------------------------------- /app/LauncherBackground.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/LauncherBackground.afphoto -------------------------------------------------------------------------------- /app/LauncherBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/LauncherBackground.png -------------------------------------------------------------------------------- /app/LauncherBackground.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/LauncherForeground.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/LauncherForeground.afdesign -------------------------------------------------------------------------------- /app/LauncherForeground.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.parcelize) 5 | alias(libs.plugins.kotlin.atomicfu) 6 | alias(libs.plugins.kotlin.compose) 7 | alias(libs.plugins.bugsnag.android) 8 | } 9 | 10 | android { 11 | compileSdk = 35 12 | 13 | defaultConfig { 14 | applicationId = "tk.zwander.rootactivitylauncher" 15 | namespace = "tk.zwander.rootactivitylauncher" 16 | minSdk = 21 17 | targetSdk = 35 18 | versionCode = 33 19 | versionName = versionCode.toString() 20 | 21 | extensions.getByType(BasePluginExtension::class.java).archivesName.set("RootActivityLauncher_${versionCode}") 22 | } 23 | 24 | buildTypes { 25 | release { 26 | signingConfig = signingConfigs["debug"] 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_11 32 | targetCompatibility = JavaVersion.VERSION_11 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = "11" 37 | } 38 | 39 | buildFeatures { 40 | viewBinding = true 41 | compose = true 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation(fileTree("libs") { include("*.jar") }) 47 | implementation(libs.kotlin.stdlib) 48 | implementation(libs.kotlin.reflect) 49 | implementation(libs.atomicfu) 50 | 51 | implementation(libs.core.ktx) 52 | implementation(libs.appcompat) 53 | implementation(libs.preference.ktx) 54 | 55 | implementation(libs.compose.material3) 56 | implementation(libs.compose.ui.tooling.preview) 57 | implementation(libs.compose.runtime) 58 | implementation(libs.compose.material) 59 | implementation(libs.activity.compose) 60 | implementation(libs.compose.theme.adapter3) 61 | implementation(libs.datastore.preferences) 62 | 63 | implementation(libs.coil) 64 | implementation(libs.coil.compose) 65 | implementation(libs.composetooltip) 66 | 67 | implementation(libs.google.material) 68 | implementation(libs.gson) 69 | 70 | implementation(libs.shizuku.api) 71 | implementation(libs.shizuku.provider) 72 | implementation(libs.hiddenapibypass) 73 | implementation(libs.patreonSupportersRetrieval) 74 | implementation(libs.taskerpluginlibrary) 75 | 76 | implementation(libs.kotlinx.coroutines.core) 77 | implementation(libs.kotlinx.coroutines.android) 78 | implementation(libs.libsuperuser) 79 | implementation(libs.progressCircula) 80 | 81 | implementation(libs.bugsnag.android) 82 | implementation(libs.dhizuku.api) 83 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 66 | 67 | 68 | 69 | 70 | 71 | 78 | 79 | 85 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 101 | 103 | 106 | 107 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/App.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher 2 | 3 | import android.app.Application 4 | import android.os.Build 5 | import com.bugsnag.android.Bugsnag 6 | import org.lsposed.hiddenapibypass.HiddenApiBypass 7 | 8 | class App : Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | 12 | Bugsnag.start(this) 13 | 14 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 15 | HiddenApiBypass.setHiddenApiExemptions("L") 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/activities/ShortcutLaunchActivity.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.activities 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.content.pm.ShortcutInfoCompat 8 | import androidx.core.content.pm.ShortcutManagerCompat 9 | import androidx.core.graphics.drawable.IconCompat 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.runBlocking 12 | import tk.zwander.rootactivitylauncher.data.component.ComponentType 13 | import tk.zwander.rootactivitylauncher.util.determineComponentNamePackage 14 | import tk.zwander.rootactivitylauncher.util.findExtrasForComponent 15 | import tk.zwander.rootactivitylauncher.util.launch.launch 16 | 17 | class ShortcutLaunchActivity : AppCompatActivity() { 18 | companion object { 19 | const val EXTRA_COMPONENT_KEY = "component_key" 20 | const val EXTRA_COMPONENT_TYPE = "component_type" 21 | 22 | fun createShortcut( 23 | context: Context, 24 | label: CharSequence, 25 | icon: IconCompat?, 26 | componentKey: String, 27 | componentType: ComponentType 28 | ) { 29 | with(context) { 30 | val shortcut = Intent(this, ShortcutLaunchActivity::class.java) 31 | shortcut.action = Intent.ACTION_MAIN 32 | shortcut.putExtra(EXTRA_COMPONENT_KEY, componentKey) 33 | shortcut.putExtra(EXTRA_COMPONENT_TYPE, componentType.serialize()) 34 | 35 | val info = ShortcutInfoCompat.Builder(this, componentKey) 36 | .setIcon(icon) 37 | .setShortLabel(label.takeIf { it.isNotBlank() } ?: componentKey) 38 | .setLongLabel("$label: $componentKey") 39 | .setIntent(shortcut) 40 | .build() 41 | 42 | ShortcutManagerCompat.requestPinShortcut( 43 | this, 44 | info, 45 | null 46 | ) 47 | } 48 | } 49 | } 50 | 51 | private val componentKey by lazy { intent.getStringExtra(EXTRA_COMPONENT_KEY) } 52 | private val componentType by lazy { 53 | ComponentType.deserialize( 54 | intent.getStringExtra( 55 | EXTRA_COMPONENT_TYPE 56 | ) 57 | ) 58 | } 59 | 60 | override fun onCreate(savedInstanceState: Bundle?) { 61 | super.onCreate(savedInstanceState) 62 | 63 | if (componentKey.isNullOrBlank() || componentType == null) { 64 | finish() 65 | return 66 | } 67 | 68 | val extras = findExtrasForComponent(componentKey!!) 69 | val globalExtras = findExtrasForComponent(determineComponentNamePackage(componentKey!!)) 70 | 71 | runBlocking(Dispatchers.IO) { 72 | launch( 73 | componentType!!, 74 | globalExtras + extras, 75 | componentKey!! 76 | ) 77 | } 78 | 79 | finish() 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/activities/tasker/TaskerLaunchComponentConfigureActivity.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.activities.tasker 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig 6 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 7 | import tk.zwander.rootactivitylauncher.MainActivity 8 | import tk.zwander.rootactivitylauncher.data.component.ComponentType 9 | import tk.zwander.rootactivitylauncher.data.tasker.TaskerLaunchComponentInfo 10 | import tk.zwander.rootactivitylauncher.util.tasker.TaskerLaunchComponentHelper 11 | 12 | class TaskerLaunchComponentConfigureActivity : MainActivity(), TaskerPluginConfig { 13 | override val context: Context 14 | get() = this 15 | override val inputForTasker: TaskerInput 16 | get() = TaskerInput(TaskerLaunchComponentInfo(selectedItem?.first?.serialize(), selectedItem?.second?.flattenToString())) 17 | 18 | override val isForTasker = true 19 | override var selectedItem: Pair? = null 20 | set(value) { 21 | field = value 22 | 23 | if (value != null) { 24 | TaskerLaunchComponentHelper(this).finishForTasker() 25 | } 26 | } 27 | 28 | override fun assignFromInput(input: TaskerInput) { 29 | val type = ComponentType.deserialize(input.regular.type) 30 | val cmp = ComponentName.unflattenFromString(input.regular.component ?: "") 31 | 32 | selectedItem = if (type == null || cmp == null) { 33 | null 34 | } else { 35 | type to cmp 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/BaseOption.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data 2 | 3 | import androidx.annotation.StringRes 4 | 5 | abstract class BaseOption(@StringRes val labelRes: Int) 6 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/ExtraInfo.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data 2 | 3 | import android.content.ComponentName 4 | import android.content.Intent 5 | import android.os.Parcelable 6 | import androidx.core.net.toUri 7 | import kotlinx.parcelize.Parcelize 8 | import tk.zwander.rootactivitylauncher.R 9 | 10 | @Parcelize 11 | data class ExtraInfo( 12 | var key: String, 13 | var value: String, 14 | var type: ExtraType? = ExtraType.STRING 15 | ) : Parcelable { 16 | val safeType: ExtraType 17 | get() = type ?: ExtraType.STRING 18 | } 19 | 20 | enum class ExtraType(val value: String, val nameRes: Int) { 21 | INTEGER("integer", R.string.integer) { 22 | override val shellArgName: String 23 | get() = "ei" 24 | 25 | override fun putExtra(intent: Intent, key: String, value: String) { 26 | intent.putExtra(key, value.toIntOrNull()) 27 | } 28 | }, 29 | BOOLEAN("boolean", R.string.bool) { 30 | override val shellArgName: String 31 | get() = "ez" 32 | 33 | override fun putExtra(intent: Intent, key: String, value: String) { 34 | intent.putExtra(key, value.toBooleanStrictOrNull()) 35 | } 36 | }, 37 | STRING("string", R.string.str) { 38 | override val shellArgName: String 39 | get() = "es" 40 | 41 | override fun putExtra(intent: Intent, key: String, value: String) { 42 | intent.putExtra(key, value) 43 | } 44 | }, 45 | BYTE("byte", R.string.byte_name) { 46 | override val shellArgName: String 47 | get() = "ei" 48 | 49 | override fun putExtra(intent: Intent, key: String, value: String) { 50 | intent.putExtra(key, value.toByteOrNull()) 51 | } 52 | }, 53 | CHAR("char", R.string.char_name) { 54 | override val shellArgName: String 55 | get() = "ei" 56 | 57 | override fun putExtra(intent: Intent, key: String, value: String) { 58 | intent.putExtra(key, value.toIntOrNull()) 59 | } 60 | }, 61 | LONG("long", R.string.long_name) { 62 | override val shellArgName: String 63 | get() = "el" 64 | 65 | override fun putExtra(intent: Intent, key: String, value: String) { 66 | intent.putExtra(key, value.toLongOrNull()) 67 | } 68 | }, 69 | FLOAT("float", R.string.float_name) { 70 | override val shellArgName: String 71 | get() = "ef" 72 | 73 | override fun putExtra(intent: Intent, key: String, value: String) { 74 | intent.putExtra(key, value.toFloatOrNull()) 75 | } 76 | }, 77 | DOUBLE("double", R.string.double_name) { 78 | override val shellArgName: String 79 | get() = "ed" 80 | 81 | override fun putExtra(intent: Intent, key: String, value: String) { 82 | intent.putExtra(key, value.toDoubleOrNull()) 83 | } 84 | }, 85 | SHORT("short", R.string.short_name) { 86 | override val shellArgName: String 87 | get() = "ei" 88 | 89 | override fun putExtra(intent: Intent, key: String, value: String) { 90 | intent.putExtra(key, value.toShortOrNull()) 91 | } 92 | }, 93 | INT_LIST("int_list", R.string.int_al) { 94 | override val shellArgName: String 95 | get() = "eial" 96 | 97 | override fun putExtra(intent: Intent, key: String, value: String) { 98 | intent.putExtra(key, ArrayList(value.split(",").mapNotNull { it.toIntOrNull() })) 99 | } 100 | }, 101 | STRING_LIST("string_list", R.string.string_al) { 102 | override val shellArgName: String 103 | get() = "esal" 104 | 105 | override fun putExtra(intent: Intent, key: String, value: String) { 106 | intent.putExtra( 107 | key, 108 | ArrayList(value.split(Regex("(?> 47 | get() = gson.fromJson( 48 | prefs.getString(KEY_EXTRAS, ""), 49 | object : TypeToken>>() {}.type 50 | ) ?: HashMap() 51 | set(value) { 52 | prefs.edit { 53 | putString( 54 | KEY_EXTRAS, 55 | gson.toJson(value) 56 | ) 57 | } 58 | } 59 | var actions: HashMap 60 | get() = gson.fromJson( 61 | prefs.getString(KEY_ACTIONS, ""), 62 | object : TypeToken>() {}.type 63 | ) ?: HashMap() 64 | set(value) { 65 | prefs.edit { 66 | putString( 67 | KEY_ACTIONS, 68 | gson.toJson(value) 69 | ) 70 | } 71 | } 72 | var datas: HashMap 73 | get() = gson.fromJson( 74 | prefs.getString(KEY_DATAS, ""), 75 | object : TypeToken>() {}.type 76 | ) ?: HashMap() 77 | set(value) { 78 | prefs.edit { 79 | putString( 80 | KEY_DATAS, 81 | gson.toJson(value) 82 | ) 83 | } 84 | } 85 | var categories: HashMap> 86 | get() = gson.fromJson( 87 | prefs.getString(KEY_CATEGORIES, ""), 88 | object : TypeToken>>() {}.type 89 | ) ?: HashMap() 90 | set(value) { 91 | prefs.edit { 92 | putString( 93 | KEY_CATEGORIES, 94 | gson.toJson(value) 95 | ) 96 | } 97 | } 98 | 99 | val favoriteActivities: Flow> 100 | get() = store.data.map { ArrayList(it[KEY_FAVORITE_ACTIVITIES] ?: setOf()) } 101 | val favoriteReceivers: Flow> 102 | get() = store.data.map { ArrayList(it[KEY_FAVORITE_RECEIVERS] ?: setOf()) } 103 | val favoriteServices: Flow> 104 | get() = store.data.map { ArrayList(it[KEY_FAVORITE_SERVICES] ?: setOf()) } 105 | 106 | suspend fun updateFavoriteActivities(activities: List) { 107 | updateFavorites(KEY_FAVORITE_ACTIVITIES, activities) 108 | } 109 | 110 | suspend fun updateFavoriteReceivers(receivers: List) { 111 | updateFavorites(KEY_FAVORITE_RECEIVERS, receivers) 112 | } 113 | 114 | suspend fun updateFavoriteServices(services: List) { 115 | updateFavorites(KEY_FAVORITE_SERVICES, services) 116 | } 117 | 118 | private suspend fun updateFavorites(key: Preferences.Key>, items: List) { 119 | store.edit { 120 | it[key] = items.toSet() 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/Query.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data 2 | 3 | import android.content.Context 4 | import tk.zwander.rootactivitylauncher.data.component.BaseComponentInfo 5 | 6 | sealed class Query(val raw: String) { 7 | val isBlank: Boolean 8 | get() = raw.isBlank() 9 | 10 | abstract fun checkMatch(context: Context, data: BaseComponentInfo, advancedMatch: Boolean): Boolean 11 | 12 | data class StringQuery(val query: String) : Query(query) { 13 | override fun checkMatch(context: Context, data: BaseComponentInfo, advancedMatch: Boolean): Boolean { 14 | return data.info.name.contains(query, true) 15 | || (data.label.contains(query, true)) 16 | || advancedMatch 17 | } 18 | } 19 | 20 | data class RegexQuery(val query: Regex) : Query(query.pattern) { 21 | override fun checkMatch( 22 | context: Context, 23 | data: BaseComponentInfo, 24 | advancedMatch: Boolean 25 | ): Boolean { 26 | return query.run { 27 | containsMatchIn(data.info.name) 28 | || containsMatchIn(data.label) 29 | } || advancedMatch 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/SortMode.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data 2 | 3 | import tk.zwander.rootactivitylauncher.R 4 | 5 | sealed class SortMode(labelRes: Int) : BaseOption(labelRes) { 6 | data object SortByName : SortMode(R.string.sort_by_name) 7 | data object SortByInstalledDate : SortMode(R.string.sort_by_installed) 8 | data object SortByUpdatedDate : SortMode(R.string.sort_by_updated) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/SortOrder.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data 2 | 3 | import tk.zwander.rootactivitylauncher.R 4 | 5 | sealed class SortOrder(labelRes: Int) : BaseOption(labelRes) { 6 | abstract fun applyOrder(input: Int): Int 7 | 8 | data object Ascending : SortOrder(R.string.sort_ascending) { 9 | override fun applyOrder(input: Int): Int { 10 | return input 11 | } 12 | } 13 | data object Descending : SortOrder(R.string.sort_descending) { 14 | override fun applyOrder(input: Int): Int { 15 | return -input 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/component/ActivityInfo.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.component 2 | 3 | import android.content.pm.ActivityInfo 4 | 5 | data class ActivityInfo( 6 | override val info: ActivityInfo, 7 | override val label: CharSequence 8 | ) : BaseComponentInfo(info, label) -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/component/Availability.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.component 2 | 3 | import tk.zwander.rootactivitylauncher.R 4 | 5 | enum class Availability(val labelRes: Int, val tintRes: Int) { 6 | EXPORTED(R.string.exported, R.color.colorExported), 7 | PERMISSION_REQUIRED(R.string.permission_required, R.color.colorNeedsPermission), 8 | UNEXPORTED(R.string.unexported, R.color.colorUnexported), 9 | NA(0, 0) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/component/BaseComponentInfo.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.component 2 | 3 | import android.content.ComponentName 4 | import android.content.pm.ComponentInfo 5 | import tk.zwander.rootactivitylauncher.util.constructComponentKey 6 | import java.util.Objects 7 | 8 | sealed class BaseComponentInfo( 9 | open val info: ComponentInfo, 10 | open val label: CharSequence 11 | ) : Comparable { 12 | val component by lazy { ComponentName(info.packageName, info.name) } 13 | 14 | fun type(): ComponentType { 15 | return when(this) { 16 | is ActivityInfo -> ComponentType.ACTIVITY 17 | is ReceiverInfo -> ComponentType.RECEIVER 18 | is ServiceInfo -> ComponentType.SERVICE 19 | } 20 | } 21 | 22 | override fun compareTo(other: BaseComponentInfo): Int { 23 | return label.toString().compareTo(other.label.toString(), true).run { 24 | if (this == 0) constructComponentKey(info).compareTo(constructComponentKey(other.info), true) 25 | else this 26 | } 27 | } 28 | 29 | override fun equals(other: Any?): Boolean { 30 | return other is BaseComponentInfo 31 | && component == other.component 32 | && label == other.label 33 | } 34 | 35 | override fun hashCode(): Int { 36 | return Objects.hash(component, label) 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/component/ComponentType.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.component 2 | 3 | enum class ComponentType { 4 | ACTIVITY, 5 | SERVICE, 6 | RECEIVER; 7 | 8 | fun serialize(): String = name 9 | 10 | companion object { 11 | fun deserialize(name: String?) = if (name != null) valueOf(name) else null 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/component/ReceiverInfo.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.component 2 | 3 | import android.content.pm.ActivityInfo 4 | 5 | data class ReceiverInfo( 6 | override val info: ActivityInfo, 7 | override val label: CharSequence 8 | ) : BaseComponentInfo(info, label) -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/component/ServiceInfo.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.component 2 | 3 | import android.content.pm.ServiceInfo 4 | 5 | data class ServiceInfo( 6 | override val info: ServiceInfo, 7 | override val label: CharSequence 8 | ) : BaseComponentInfo(info, label) -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/model/AppModel.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.model 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageInfo 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import tk.zwander.rootactivitylauncher.data.component.ActivityInfo 9 | import tk.zwander.rootactivitylauncher.data.component.ReceiverInfo 10 | import tk.zwander.rootactivitylauncher.data.component.ServiceInfo 11 | import java.util.Objects 12 | 13 | data class AppModel( 14 | val pInfo: PackageInfo, 15 | val info: ApplicationInfo? = pInfo.applicationInfo, 16 | val label: CharSequence, 17 | override val mainModel: MainModel, 18 | override val scope: CoroutineScope, 19 | override val context: Context, 20 | ) : BaseInfoModel(context, scope, mainModel) { 21 | override val initialActivitiesSize = MutableStateFlow(pInfo.activities?.size ?: 0) 22 | override val initialServicesSize = MutableStateFlow(pInfo.services?.size ?: 0) 23 | override val initialReceiversSize = MutableStateFlow(pInfo.receivers?.size ?: 0) 24 | 25 | override fun equals(other: Any?): Boolean { 26 | return other is AppModel 27 | && pInfo.packageName == other.pInfo.packageName 28 | && super.equals(other) 29 | && activitiesSize.value == other.activitiesSize.value 30 | && servicesSize.value == other.servicesSize.value 31 | && receiversSize.value == other.receiversSize.value 32 | && filteredActivities.value == other.filteredActivities.value 33 | && filteredServices.value == other.filteredServices.value 34 | && filteredReceivers.value == other.filteredReceivers.value 35 | } 36 | 37 | override fun hashCode(): Int { 38 | return info?.packageName.hashCode() + 39 | 31 * super.hashCode() + 40 | Objects.hash( 41 | activitiesSize.value, 42 | servicesSize.value, 43 | receiversSize.value, 44 | filteredActivities.value, 45 | filteredServices.value, 46 | filteredReceivers.value 47 | ) 48 | } 49 | 50 | override suspend fun performActivityLoad(progress: (suspend () -> Unit)?): Collection { 51 | return pInfo.activities.loadItems( 52 | progress = progress 53 | ) { input, label -> ActivityInfo(input, label.ifBlank { this.label }) } 54 | .toSortedSet() 55 | } 56 | 57 | override suspend fun performServiceLoad(progress: (suspend () -> Unit)?): Collection { 58 | return pInfo.services.loadItems( 59 | progress = progress 60 | ) { input, label -> ServiceInfo(input, label.ifBlank { this.label }) } 61 | .toSortedSet() 62 | } 63 | 64 | override suspend fun performReceiverLoad(progress: (suspend () -> Unit)?): Collection { 65 | return pInfo.receivers.loadItems( 66 | progress = progress 67 | ) { input, label -> ReceiverInfo(input, label.ifBlank { this.label }) } 68 | .toSortedSet() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/model/ExtrasDialogModel.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.model 2 | 3 | import android.content.Context 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import tk.zwander.rootactivitylauncher.data.ExtraInfo 6 | import tk.zwander.rootactivitylauncher.util.findActionForComponent 7 | import tk.zwander.rootactivitylauncher.util.findCategoriesForComponent 8 | import tk.zwander.rootactivitylauncher.util.findDataForComponent 9 | import tk.zwander.rootactivitylauncher.util.findExtrasForComponent 10 | import java.util.UUID 11 | 12 | class ExtrasDialogModel(private val context: Context, private val componentKey: String) { 13 | val extras = MutableStateFlow>>( 14 | ArrayList>().apply { 15 | addAll(context.findExtrasForComponent(componentKey).map { 16 | UUID.randomUUID() to it 17 | }) 18 | 19 | add(UUID.randomUUID() to ExtraInfo("", "")) 20 | } 21 | ) 22 | 23 | val categories = MutableStateFlow>>( 24 | ArrayList>().apply { 25 | addAll(context.findCategoriesForComponent(componentKey).map { 26 | UUID.randomUUID() to it 27 | }) 28 | 29 | add(UUID.randomUUID() to "") 30 | } 31 | ) 32 | 33 | val action = MutableStateFlow(context.findActionForComponent(componentKey)) 34 | val data = MutableStateFlow(context.findDataForComponent(componentKey)) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/model/FavoriteModel.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.model 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.pm.PackageItemInfo 7 | import android.content.pm.PackageManager 8 | import android.content.pm.PackageManager.NameNotFoundException 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.first 13 | import kotlinx.coroutines.launch 14 | import tk.zwander.rootactivitylauncher.data.component.ActivityInfo 15 | import tk.zwander.rootactivitylauncher.data.component.ReceiverInfo 16 | import tk.zwander.rootactivitylauncher.data.component.ServiceInfo 17 | import tk.zwander.rootactivitylauncher.util.getActivityInfoCompat 18 | import tk.zwander.rootactivitylauncher.util.getReceiverInfoCompat 19 | import tk.zwander.rootactivitylauncher.util.getServiceInfoCompat 20 | 21 | data class FavoriteModel( 22 | val activityKeys: Flow>, 23 | val serviceKeys: Flow>, 24 | val receiverKeys: Flow>, 25 | override val context: Context, 26 | override val scope: CoroutineScope, 27 | override val mainModel: MainModel 28 | ) : BaseInfoModel(context, scope, mainModel) { 29 | override val initialActivitiesSize = MutableStateFlow(0) 30 | override val initialServicesSize = MutableStateFlow(0) 31 | override val initialReceiversSize = MutableStateFlow(0) 32 | 33 | init { 34 | scope.launch { 35 | activityKeys.collect { 36 | _loadedActivities.clear() 37 | initialActivitiesSize.value = it.size 38 | hasLoadedActivities.value = false 39 | } 40 | } 41 | 42 | scope.launch { 43 | serviceKeys.collect { 44 | _loadedServices.clear() 45 | initialServicesSize.value = it.size 46 | hasLoadedServices.value = false 47 | } 48 | } 49 | 50 | scope.launch { 51 | receiverKeys.collect { 52 | _loadedReceivers.clear() 53 | initialReceiversSize.value = it.size 54 | hasLoadedReceivers.value = false 55 | } 56 | } 57 | } 58 | 59 | @SuppressLint("InlinedApi") 60 | override suspend fun performActivityLoad(progress: (suspend () -> Unit)?): Collection { 61 | val activityInfos = activityKeys.first().mapNotNull { key -> 62 | context.getInfoFromKey(key) { 63 | packageManager.getActivityInfoCompat(it, PackageManager.MATCH_DISABLED_COMPONENTS) 64 | } 65 | }.toTypedArray() 66 | 67 | return activityInfos.loadItems(progress) { input, label -> 68 | ActivityInfo(input, label.ifBlank { 69 | packageManager.getApplicationLabel(input.applicationInfo) 70 | }) 71 | }.sortedBy { it.label.toString().lowercase() } 72 | } 73 | 74 | @SuppressLint("InlinedApi") 75 | override suspend fun performServiceLoad(progress: (suspend () -> Unit)?): Collection { 76 | val activityInfos = serviceKeys.first().mapNotNull { key -> 77 | context.getInfoFromKey(key) { 78 | packageManager.getServiceInfoCompat(it, PackageManager.MATCH_DISABLED_COMPONENTS) 79 | } 80 | }.toTypedArray() 81 | 82 | return activityInfos.loadItems(progress) { input, label -> 83 | ServiceInfo(input, label.ifBlank { 84 | packageManager.getApplicationLabel(input.applicationInfo) 85 | }) 86 | }.sortedBy { it.label.toString().lowercase() } 87 | } 88 | 89 | @SuppressLint("InlinedApi") 90 | override suspend fun performReceiverLoad(progress: (suspend () -> Unit)?): Collection { 91 | val activityInfos = receiverKeys.first().mapNotNull { key -> 92 | context.getInfoFromKey(key) { 93 | packageManager.getReceiverInfoCompat(it, PackageManager.MATCH_DISABLED_COMPONENTS) 94 | } 95 | }.toTypedArray() 96 | 97 | return activityInfos.loadItems(progress) { input, label -> 98 | ReceiverInfo(input, label.ifBlank { 99 | packageManager.getApplicationLabel(input.applicationInfo) 100 | }) 101 | }.sortedBy { it.label.toString().lowercase() } 102 | } 103 | 104 | private fun Context.getInfoFromKey(key: String, getter: Context.(ComponentName) -> T): T? { 105 | val componentName = ComponentName.unflattenFromString(key)!! 106 | 107 | return try { 108 | getter(componentName) 109 | } catch (e: NameNotFoundException) { 110 | if (componentName.className.contains(".")) { 111 | val lastIndex = key.lastIndexOf(".") 112 | getInfoFromKey(key.replaceRange(lastIndex..lastIndex, "$"), getter) 113 | } else { 114 | null 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/data/tasker/TaskerLaunchComponentInfo.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.data.tasker 2 | 3 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInputField 4 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot 5 | 6 | @TaskerInputRoot 7 | class TaskerLaunchComponentInfo @JvmOverloads constructor( 8 | @field:TaskerInputField("type") var type: String? = null, 9 | @field:TaskerInputField("component") var component: String? = null 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/AdvancedSearcher.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.pm.ActivityInfo 4 | import android.content.pm.ComponentInfo 5 | import android.content.pm.ServiceInfo 6 | import tk.zwander.rootactivitylauncher.data.model.AppModel 7 | 8 | object AdvancedSearcher { 9 | enum class LogicMode { 10 | AND, 11 | OR; 12 | 13 | companion object { 14 | fun fromString(source: String, def: LogicMode): LogicMode { 15 | return entries.find { 16 | it.name.lowercase() == source.lowercase() 17 | } ?: def 18 | } 19 | } 20 | } 21 | 22 | private const val MODE_DELIMITER = ";" 23 | private const val ITEM_DELIMITER = "," 24 | private const val EXP_DELIMITER = " " 25 | private const val TYPE_DELIMITER = ":" 26 | 27 | private const val HAS_PERMISSION = "has-permission" 28 | private const val REQUIRES_PERMISSION = "requires-permission" 29 | private const val DECLARES_PERMISSION = "declares-permission" 30 | private const val REQUIRES_FEATURE = "requires-feature" 31 | 32 | /** 33 | * Expects a [query] string in a format like: 34 | * ` [type]:[AND;|OR;]item1,item2,item3,... `. 35 | * 36 | * If neither AND; nor OR; are found, the default mode is [LogicMode.AND]. 37 | */ 38 | private fun itemMatch( 39 | query: String, 40 | type: String, 41 | checker: (LogicMode, List) -> Boolean 42 | ): Boolean { 43 | if (!query.contains("$type$TYPE_DELIMITER")) return false 44 | 45 | val i = query.substringAfter("$type$TYPE_DELIMITER").substringBefore(EXP_DELIMITER) 46 | val (mode, items) = try { 47 | if (i.contains(MODE_DELIMITER)) { 48 | i.split(MODE_DELIMITER).run { 49 | LogicMode.fromString(this[0], LogicMode.AND) to 50 | this[1].split(ITEM_DELIMITER) 51 | } 52 | } else { 53 | LogicMode.AND to i.split(ITEM_DELIMITER) 54 | } 55 | } catch (_: Exception) { 56 | return false 57 | } 58 | 59 | return checker(mode, items) 60 | } 61 | 62 | private fun logicMatch(mode: LogicMode, items: List, availableItems: List): Boolean { 63 | return if (mode == LogicMode.OR) { 64 | items.any { availableItems.contains(it) } 65 | } else { 66 | items.all { availableItems.contains(it) } 67 | } 68 | } 69 | 70 | fun matchesHasPermission(query: String, data: AppModel): Boolean { 71 | return itemMatch(query, HAS_PERMISSION) { mode, items -> 72 | val p = data.pInfo.requestedPermissions ?: return@itemMatch false 73 | 74 | logicMatch(mode, items, p.toList()) 75 | } 76 | } 77 | 78 | fun matchesRequiresPermission(query: String, data: AppModel): Boolean { 79 | return matchesRequiresPermission(query, data.info?.permission) 80 | } 81 | 82 | fun matchesRequiresPermission(query: String, componentInfo: ComponentInfo): Boolean { 83 | val permission = when (componentInfo) { 84 | is ActivityInfo -> componentInfo.permission 85 | is ServiceInfo -> componentInfo.permission 86 | else -> null 87 | } 88 | 89 | return matchesRequiresPermission(query, permission) 90 | } 91 | 92 | private fun matchesRequiresPermission(query: String, requiredPermission: String?): Boolean { 93 | return itemMatch(query, REQUIRES_PERMISSION) { _, items -> 94 | // Always an OR check, so we don't care about any mode given. 95 | items.contains(requiredPermission) 96 | } 97 | } 98 | 99 | fun matchesDeclaresPermission(query: String, data: AppModel): Boolean { 100 | return itemMatch(query, DECLARES_PERMISSION) { mode, items -> 101 | val declaredPermissions = data.pInfo.permissions?.map { it.name } ?: return@itemMatch false 102 | 103 | logicMatch(mode, items, declaredPermissions) 104 | } 105 | } 106 | 107 | fun matchesRequiresFeature(query: String, data: AppModel): Boolean { 108 | return itemMatch(query, REQUIRES_FEATURE) { mode, items -> 109 | val requiredFeatures = data.pInfo.reqFeatures?.map { it.name } ?: return@itemMatch false 110 | 111 | logicMatch(mode, items, requiredFeatures) 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/CollectionUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.util.SparseArray 4 | import androidx.core.util.forEach 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Deferred 7 | import kotlinx.coroutines.async 8 | import kotlinx.coroutines.awaitAll 9 | import tk.zwander.rootactivitylauncher.data.model.AppModel 10 | import tk.zwander.rootactivitylauncher.data.model.BaseInfoModel 11 | import kotlin.coroutines.CoroutineContext 12 | import kotlin.coroutines.EmptyCoroutineContext 13 | 14 | fun SparseArray.map(transform: (T) -> R): List { 15 | return mapTo(ArrayList(), transform) 16 | } 17 | 18 | fun > SparseArray.mapTo(destination: C, transform: (T) -> R): C { 19 | forEach { _, any -> 20 | destination.add(transform(any)) 21 | } 22 | 23 | return destination 24 | } 25 | 26 | suspend fun Collection.forEachParallel(context: CoroutineContext = EmptyCoroutineContext, scope: CoroutineScope, block: suspend CoroutineScope.(T) -> Unit) { 27 | val jobs = ArrayList>(size) 28 | forEach { 29 | jobs.add( 30 | scope.async(context) { 31 | block(it) 32 | } 33 | ) 34 | } 35 | jobs.awaitAll() 36 | } 37 | 38 | suspend fun Array.forEachParallel(context: CoroutineContext = EmptyCoroutineContext, scope: CoroutineScope, block: suspend CoroutineScope.(T) -> Unit) { 39 | val jobs = ArrayList>(size) 40 | forEach { 41 | jobs.add( 42 | scope.async(context) { 43 | block(it) 44 | } 45 | ) 46 | } 47 | jobs.awaitAll() 48 | } 49 | 50 | fun Collection.distinctByPackageName(): List { 51 | return distinctBy { 52 | if (it is AppModel) it.pInfo.packageName else "favorite" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/ComponentUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.ActivityInfo 8 | import android.content.pm.ApplicationInfo 9 | import android.content.pm.ComponentInfo 10 | import android.content.pm.PackageItemInfo 11 | import android.content.pm.PackageManager 12 | import android.os.Build 13 | import android.provider.Settings 14 | import androidx.core.net.toUri 15 | import tk.zwander.rootactivitylauncher.data.ExtraInfo 16 | import tk.zwander.rootactivitylauncher.data.prefs 17 | 18 | fun determineComponentNamePackage(componentName: String): String { 19 | val component = ComponentName.unflattenFromString(componentName) 20 | 21 | return component?.packageName ?: componentName 22 | } 23 | 24 | fun Context.findExtrasForComponent(componentName: String): List { 25 | val extras = ArrayList() 26 | 27 | prefs.extras[componentName]?.let { extras.addAll(it) } 28 | 29 | return extras 30 | } 31 | 32 | fun Context.updateExtrasForComponent(componentName: String, extras: List) { 33 | val map = prefs.extras 34 | 35 | map[componentName] = extras 36 | prefs.extras = map 37 | } 38 | 39 | fun Context.findActionForComponent(componentName: String): String { 40 | val pkg = determineComponentNamePackage(componentName) 41 | 42 | return prefs.actions[componentName] ?: (prefs.actions[pkg] ?: Intent.ACTION_MAIN) 43 | } 44 | 45 | fun Context.updateActionForComponent(componentName: String, action: String?) { 46 | val map = prefs.actions 47 | 48 | map[componentName] = if (action.isNullOrBlank()) null else action 49 | prefs.actions = map 50 | } 51 | 52 | fun Context.findDataForComponent(componentName: String): String? { 53 | val pkg = determineComponentNamePackage(componentName) 54 | 55 | return prefs.datas[componentName] ?: prefs.datas[pkg] 56 | } 57 | 58 | fun Context.updateDataForComponent(componentName: String, data: String?) { 59 | val map = prefs.datas 60 | 61 | map[componentName] = if (data.isNullOrBlank()) null else data 62 | prefs.datas = map 63 | } 64 | 65 | fun Context.findCategoriesForComponent(componentName: String): MutableList { 66 | val pkg = determineComponentNamePackage(componentName) 67 | 68 | return prefs.categories[componentName] ?: prefs.categories[pkg] ?: mutableListOf() 69 | } 70 | 71 | fun Context.updateCategoriesForComponent(componentName: String, categories: List) { 72 | val filtered = categories.filterNot { it.isNullOrBlank() } 73 | val map = prefs.categories 74 | 75 | map[componentName] = ArrayList(filtered) 76 | prefs.categories = map 77 | } 78 | 79 | fun constructComponentKey(component: PackageItemInfo): String { 80 | return constructComponentKey(component.packageName, component.name) 81 | } 82 | 83 | fun constructComponentKey(packageName: String, componentName: String): String { 84 | return "$packageName/$componentName" 85 | } 86 | 87 | fun Context.openAppInfo(packageName: String) { 88 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 89 | intent.data = "package:$packageName".toUri() 90 | 91 | try { 92 | startActivity(intent) 93 | } catch (_: Exception) {} 94 | } 95 | 96 | fun ActivityInfo.persistableModeToString(): String { 97 | return when (persistableMode) { 98 | ActivityInfo.PERSIST_ROOT_ONLY -> "PERSIST_ROOT_ONLY" 99 | ActivityInfo.PERSIST_NEVER -> "PERSIST_NEVER" 100 | ActivityInfo.PERSIST_ACROSS_REBOOTS -> "PERSIST_ACROSS_REBOOTS" 101 | else -> "UNKNOWN=$persistableMode" 102 | } 103 | } 104 | 105 | val ActivityInfo.manifestMinAspectRatioCompat: Float 106 | @SuppressLint("PrivateApi") 107 | get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 108 | manifestMinAspectRatio 109 | } else { 110 | ActivityInfo::class.java 111 | .getDeclaredField("mMinAspectRatio") 112 | .apply { isAccessible = true } 113 | .getFloat(this) 114 | } 115 | 116 | val ActivityInfo.rMaxAspectRatio: Float 117 | @SuppressLint("PrivateApi") 118 | get() = ActivityInfo::class.java 119 | .getDeclaredField("mMaxAspectRatio") 120 | .apply { isAccessible = true } 121 | .getFloat(this) 122 | 123 | fun ComponentInfo.isActuallyEnabled(context: Context): Boolean { 124 | return applicationInfo.isActuallyEnabled(context) && try { 125 | checkEnabledSetting( 126 | context.packageManager.getComponentEnabledSetting(safeComponentName), 127 | enabled 128 | ) 129 | } catch (_: Exception) { 130 | enabled 131 | } 132 | } 133 | 134 | fun ApplicationInfo.isActuallyEnabled(context: Context): Boolean { 135 | return try { 136 | checkEnabledSetting( 137 | context.packageManager.getApplicationEnabledSetting(packageName), 138 | enabled 139 | ) 140 | } catch (_: Exception) { 141 | enabled 142 | } 143 | } 144 | 145 | fun ApplicationInfo.isSystemAppCompat(): Boolean { 146 | return (flags and ApplicationInfo.FLAG_SYSTEM) != 0 147 | } 148 | 149 | private fun checkEnabledSetting(setting: Int, default: Boolean): Boolean { 150 | return when (setting) { 151 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 152 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> true 153 | PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> default 154 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 155 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> false 156 | else -> false 157 | } 158 | } 159 | 160 | val ComponentInfo.safeComponentName: ComponentName 161 | get() = ComponentName(packageName, name) 162 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/CompositionLocals.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import tk.zwander.rootactivitylauncher.data.model.FavoriteModel 5 | import tk.zwander.rootactivitylauncher.data.model.MainModel 6 | 7 | val LocalMainModel = compositionLocalOf { error("No MainModel specified!") } 8 | val LocalFavoriteModel = compositionLocalOf { error("No FavoriteModel specified!") } 9 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/DhizukuUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.pm.PackageManager 4 | import com.rosan.dhizuku.api.Dhizuku 5 | import com.rosan.dhizuku.api.DhizukuRequestPermissionListener 6 | import kotlin.coroutines.resume 7 | import kotlin.coroutines.suspendCoroutine 8 | 9 | object DhizukuUtils { 10 | suspend fun requestDhizukuPermission(): Boolean { 11 | return suspendCoroutine { continuation -> 12 | Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() { 13 | override fun onRequestPermission(grantResult: Int) { 14 | continuation.resume(grantResult == PackageManager.PERMISSION_GRANTED) 15 | } 16 | }) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/InfoGetter.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.Context 4 | import android.os.IBinder 5 | import android.os.UserHandle 6 | import com.rosan.dhizuku.api.Dhizuku 7 | import rikka.shizuku.Shizuku 8 | 9 | interface BinderWrapper { 10 | suspend fun Context.getUidAndPackage(): Pair 11 | suspend fun wrapBinder(binder: IBinder): IBinder 12 | } 13 | 14 | interface ShizukuBinderWrapper : BinderWrapper { 15 | override suspend fun Context.getUidAndPackage(): Pair { 16 | val uid = try { 17 | Shizuku.getUid() 18 | } catch (e: IllegalStateException) { 19 | // It shouldn't be possible for the Shizuku Binder not to exist at this point, but sometimes it doesn't. 20 | // If that happens, just assume user 2000 (shell). 21 | 2000 22 | } 23 | 24 | return UserHandle.getUserId(uid) to packageManager.getPackagesForUid(uid)?.firstOrNull() 25 | } 26 | 27 | override suspend fun wrapBinder(binder: IBinder): IBinder { 28 | return rikka.shizuku.ShizukuBinderWrapper(binder) 29 | } 30 | } 31 | 32 | interface DhizukuBinderWrapper : BinderWrapper { 33 | override suspend fun Context.getUidAndPackage(): Pair { 34 | val packageName = "com.rosan.dhizuku" 35 | 36 | return UserHandle.getUserId( 37 | packageManager.getApplicationInfo(packageName, 0).uid 38 | ) to packageName 39 | } 40 | 41 | override suspend fun wrapBinder(binder: IBinder): IBinder { 42 | return Dhizuku.binderWrapper(binder) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/PackageUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.IntentFilter 6 | import android.content.pm.ActivityInfo 7 | import android.content.pm.PackageInfo 8 | import android.content.pm.PackageManager 9 | import android.content.pm.ServiceInfo 10 | import android.net.Uri 11 | import android.os.Build 12 | import android.os.DeadObjectException 13 | import android.util.Log 14 | import androidx.documentfile.provider.DocumentFile 15 | import tk.zwander.rootactivitylauncher.R 16 | import tk.zwander.rootactivitylauncher.data.model.AppModel 17 | import java.io.File 18 | 19 | // Newer Android versions say these APIs are NonNull, but older versions can return null. 20 | 21 | fun PackageManager.getInstalledPackagesCompat(flags: Int = 0): List { 22 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 23 | getInstalledPackages(PackageManager.PackageInfoFlags.of(flags.toLong())) 24 | } else { 25 | getInstalledPackages(flags) 26 | } ?: listOf() 27 | } 28 | 29 | fun PackageManager.getPackageInfoCompat(pkg: String, flags: Int = 0): PackageInfo? { 30 | return try { 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 32 | getPackageInfo(pkg, PackageManager.PackageInfoFlags.of(flags.toLong())) 33 | } else { 34 | getPackageInfo(pkg, flags) 35 | } 36 | } catch (_: PackageManager.NameNotFoundException) { 37 | null 38 | } catch (e: RuntimeException) { 39 | if (e.cause is DeadObjectException) { 40 | null 41 | } else { 42 | throw e 43 | } 44 | } 45 | } 46 | 47 | fun Context.extractApk(result: Uri, info: AppModel): List { 48 | val errors = ArrayList() 49 | 50 | val dir = DocumentFile.fromTreeUri(this, result) 51 | 52 | if (dir == null) { 53 | errors.add(Exception("Unable to get file reference for $result.")) 54 | } 55 | 56 | if (info.info == null) { 57 | errors.add(Exception(resources.getString(R.string.no_app_info_error))) 58 | return errors 59 | } 60 | 61 | val baseDir = File(info.info.sourceDir) 62 | 63 | val splits = info.info.splitSourceDirs?.mapIndexed { index, s -> 64 | val splitApk = File(s) 65 | val splitName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 66 | info.info.splitNames?.get(index) ?: splitApk.nameWithoutExtension 67 | } else splitApk.nameWithoutExtension 68 | 69 | splitName to s 70 | } 71 | 72 | val baseFile = dir?.createFile( 73 | "application/vnd.android.package-archive", 74 | info.pInfo.packageName 75 | ) 76 | 77 | if (baseFile == null) { 78 | errors.add(Exception("Unable to create file ${info.pInfo.packageName}.")) 79 | } else { 80 | try { 81 | contentResolver.openOutputStream(baseFile.uri).use { writer -> 82 | baseDir.inputStream().use { reader -> 83 | reader.copyTo(writer!!) 84 | } 85 | } 86 | } catch (e: Exception) { 87 | Log.e("RootActivityLauncher", "Extraction failed", e) 88 | errors.add(e) 89 | } 90 | } 91 | 92 | splits?.forEach { split -> 93 | val name = split.first 94 | val path = File(split.second) 95 | 96 | val file = dir?.createFile( 97 | "application/vnd.android.package-archive", 98 | "${info.pInfo.packageName}_$name" 99 | ) 100 | 101 | if (file == null) { 102 | errors.add(Exception("Unable to create file ${info.pInfo.packageName}_$name")) 103 | } else { 104 | contentResolver.openOutputStream(file.uri).use { writer -> 105 | try { 106 | path.inputStream().use { reader -> 107 | reader.copyTo(writer!!) 108 | } 109 | } catch (e: Exception) { 110 | Log.e("RootActivityLauncher", "Extraction failed", e) 111 | errors.add(e) 112 | } 113 | } 114 | } 115 | } 116 | 117 | return errors 118 | } 119 | 120 | fun PackageManager.getAllIntentFiltersCompat(packageName: String?): List { 121 | if (packageName == null) { 122 | return listOf() 123 | } 124 | 125 | return try { 126 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 127 | getAllIntentFilters(packageName) 128 | } else { 129 | listOf() 130 | } 131 | } catch (_: Throwable) { 132 | listOf() 133 | } 134 | } 135 | 136 | fun PackageManager.getActivityInfoCompat( 137 | componentName: ComponentName, 138 | flags: Int = 0 139 | ): ActivityInfo { 140 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 141 | getActivityInfo(componentName, PackageManager.ComponentInfoFlags.of(flags.toLong())) 142 | } else { 143 | getActivityInfo(componentName, flags) 144 | } 145 | } 146 | 147 | fun PackageManager.getServiceInfoCompat(componentName: ComponentName, flags: Int = 0): ServiceInfo { 148 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 149 | getServiceInfo(componentName, PackageManager.ComponentInfoFlags.of(flags.toLong())) 150 | } else { 151 | getServiceInfo(componentName, flags) 152 | } 153 | } 154 | 155 | fun PackageManager.getReceiverInfoCompat( 156 | componentName: ComponentName, 157 | flags: Int = 0 158 | ): ActivityInfo { 159 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 160 | getReceiverInfo(componentName, PackageManager.ComponentInfoFlags.of(flags.toLong())) 161 | } else { 162 | getReceiverInfo(componentName, flags) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/PermissionResultListener.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | interface PermissionResultListener { 4 | fun onPermissionResult(granted: Boolean) 5 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/PermissionUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import rikka.shizuku.Shizuku 7 | import rikka.shizuku.ShizukuProvider 8 | import kotlin.coroutines.resume 9 | import kotlin.coroutines.suspendCoroutine 10 | import kotlin.random.Random 11 | 12 | val Context.hasShizukuPermission: Boolean 13 | get() { 14 | if (!Shizuku.pingBinder()) { 15 | return false 16 | } 17 | 18 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 19 | return true 20 | } 21 | 22 | return try { 23 | if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) { 24 | checkCallingOrSelfPermission(ShizukuProvider.PERMISSION) == PackageManager.PERMISSION_GRANTED 25 | } else { 26 | Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 27 | } 28 | } catch (e: IllegalStateException) { 29 | false 30 | } 31 | } 32 | 33 | suspend fun requestShizukuPermission(): Boolean { 34 | return suspendCoroutine { coroutine -> 35 | requestShizukuPermission { granted -> 36 | coroutine.resume(granted) 37 | } 38 | } 39 | } 40 | 41 | fun requestShizukuPermission(resultListener: (Boolean) -> Unit): Boolean { 42 | return if (Shizuku.isPreV11() || Shizuku.getVersion() < 11) { 43 | false 44 | } else { 45 | val code = Random(System.currentTimeMillis()).nextInt() 46 | val listener = object : Shizuku.OnRequestPermissionResultListener { 47 | override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { 48 | resultListener(grantResult == PackageManager.PERMISSION_GRANTED) 49 | Shizuku.removeRequestPermissionResultListener(this) 50 | } 51 | } 52 | 53 | try { 54 | Shizuku.addRequestPermissionResultListener(listener) 55 | Shizuku.requestPermission(code) 56 | true 57 | } catch (e: IllegalStateException) { 58 | false 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/UIUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.net.Uri 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.calculateEndPadding 7 | import androidx.compose.foundation.layout.calculateStartPadding 8 | import androidx.compose.ui.unit.LayoutDirection 9 | import androidx.core.net.toUri 10 | import com.android.internal.R 11 | import tk.zwander.rootactivitylauncher.data.component.BaseComponentInfo 12 | 13 | fun BaseComponentInfo.getIconResourceId(): Pair { 14 | val res = info.iconResource 15 | 16 | return if (res != 0) { 17 | info.packageName to res 18 | } else { 19 | info.applicationInfo.getIconResourceId() 20 | } 21 | } 22 | 23 | fun ApplicationInfo.getIconResourceId(): Pair { 24 | val res = icon 25 | 26 | return if (res != 0) { 27 | packageName to res 28 | } else { 29 | "android" to R.drawable.sym_def_app_icon 30 | } 31 | } 32 | 33 | fun BaseComponentInfo.getCoilData(): Uri { 34 | val id = getIconResourceId() 35 | 36 | return "android.resource://${id.first}/${id.second}".toUri() 37 | } 38 | 39 | fun ApplicationInfo.getCoilData(): Uri { 40 | val id = getIconResourceId() 41 | 42 | return "android.resource://${id.first}/${id.second}".toUri() 43 | } 44 | 45 | operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = PaddingValues( 46 | start = this.calculateStartPadding(LayoutDirection.Ltr) + 47 | other.calculateStartPadding(LayoutDirection.Ltr), 48 | top = this.calculateTopPadding() + other.calculateTopPadding(), 49 | end = this.calculateEndPadding(LayoutDirection.Ltr) + 50 | other.calculateEndPadding(LayoutDirection.Ltr), 51 | bottom = this.calculateBottomPadding() + other.calculateBottomPadding(), 52 | ) 53 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.net.toUri 6 | import kotlinx.atomicfu.AtomicInt 7 | import kotlinx.atomicfu.AtomicLong 8 | 9 | val Int.hexString: String 10 | get() = Integer.toHexString(this) 11 | 12 | fun Context.launchUrl(url: String) { 13 | try { 14 | val browserIntent = 15 | Intent(Intent.ACTION_VIEW, url.toUri()) 16 | startActivity(browserIntent) 17 | } catch (_: Exception) {} 18 | } 19 | 20 | val Context.isTouchWiz: Boolean 21 | get() = packageManager.hasSystemFeature("com.samsung.feature.samsung_experience_mobile") 22 | 23 | fun updateProgress( 24 | current: AtomicInt, 25 | lastUpdateTime: AtomicLong, 26 | total: Int, 27 | setter: (newProgress: Float) -> Unit 28 | ) { 29 | val oldCurrent = current.value 30 | val newCurrent = current.incrementAndGet() 31 | 32 | val oldProgress = (oldCurrent / total.toFloat() * 100f).toInt() / 100f 33 | val newProgress = (newCurrent / total.toFloat() * 100f).toInt() / 100f 34 | 35 | val oldUpdateTime = lastUpdateTime.value 36 | val newUpdateTime = System.currentTimeMillis() 37 | 38 | if (newProgress > oldProgress && newUpdateTime > oldUpdateTime) { 39 | lastUpdateTime.value = newUpdateTime 40 | 41 | setter(newProgress) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/launch/LaunchArgs.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util.launch 2 | 3 | import android.content.Intent 4 | import android.content.IntentFilter 5 | import tk.zwander.rootactivitylauncher.data.ExtraInfo 6 | 7 | data class LaunchArgs(val intent: Intent, val extras: List, val filters: List) -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/launch/LaunchUtils.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util.launch 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | import androidx.core.net.toUri 8 | import tk.zwander.rootactivitylauncher.R 9 | import tk.zwander.rootactivitylauncher.data.ExtraInfo 10 | import tk.zwander.rootactivitylauncher.data.component.ComponentType 11 | import tk.zwander.rootactivitylauncher.data.prefs 12 | import tk.zwander.rootactivitylauncher.util.findActionForComponent 13 | import tk.zwander.rootactivitylauncher.util.findCategoriesForComponent 14 | import tk.zwander.rootactivitylauncher.util.findDataForComponent 15 | import tk.zwander.rootactivitylauncher.util.getAllIntentFiltersCompat 16 | 17 | private fun Context.createLaunchArgs(extras: List, componentKey: String): LaunchArgs { 18 | val intent = Intent(prefs.findActionForComponent(componentKey)) 19 | intent.component = ComponentName.unflattenFromString(componentKey) 20 | intent.data = prefs.findDataForComponent(componentKey)?.toUri() 21 | 22 | val filters = packageManager.getAllIntentFiltersCompat(intent.component?.packageName) 23 | 24 | prefs.findCategoriesForComponent(componentKey).forEach { category -> 25 | intent.addCategory(category) 26 | } 27 | 28 | if (extras.isNotEmpty()) { 29 | extras.forEach { extra -> 30 | extra.safeType.putExtra(intent, extra.key, extra.value) 31 | } 32 | } 33 | 34 | return LaunchArgs(intent, extras, filters) 35 | } 36 | 37 | private suspend inline fun Context.performLaunch(args: LaunchArgs): List> { 38 | val errors = mutableListOf>() 39 | 40 | T::class.sealedSubclasses 41 | .mapNotNull { it.objectInstance?.let { obj -> obj to it } } 42 | .sortedByDescending { (obj, _) -> obj.priority } 43 | .forEach { (obj, clazz) -> 44 | with (obj) { 45 | if (canRun(args)) { 46 | val latestResult = tryLaunch(args) 47 | 48 | Log.e("RootActivityLauncher", "$clazz $latestResult") 49 | 50 | if (latestResult.isEmpty()) { 51 | return listOf() 52 | } else { 53 | errors.addAll(latestResult.map { r -> clazz.simpleName!! to r }) 54 | } 55 | } 56 | } 57 | } 58 | 59 | return errors.ifEmpty { 60 | listOf( 61 | "Unknown" to Exception( 62 | resources.getString( 63 | R.string.unknown_launch_error, 64 | args.intent.component?.flattenToString() 65 | ) 66 | ) 67 | ) 68 | } 69 | } 70 | 71 | suspend fun Context.launch( 72 | type: ComponentType, 73 | extras: List, 74 | componentKey: String 75 | ): List> { 76 | val args = createLaunchArgs(extras, componentKey) 77 | 78 | return when (type) { 79 | ComponentType.ACTIVITY -> performLaunch(args) 80 | ComponentType.SERVICE -> performLaunch(args) 81 | ComponentType.RECEIVER -> performLaunch(args) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/receiver/AdminReceiver.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util.receiver 2 | 3 | import android.app.admin.DeviceAdminReceiver 4 | 5 | class AdminReceiver : DeviceAdminReceiver() -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/receiver/KnoxLicenseReceiver.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | 8 | class KnoxLicenseReceiver : BroadcastReceiver() { 9 | override fun onReceive(context: Context?, intent: Intent) { 10 | Log.e("RootActivityLauncher", "${intent.action}") 11 | 12 | intent.extras?.keySet()?.forEach { 13 | Log.e("RootActivityLauncher", "$it : ${intent.extras?.get(it)}") 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/tasker/TaskerLaunchComponentHelper.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util.tasker 2 | 3 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig 4 | import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutput 5 | import tk.zwander.rootactivitylauncher.data.tasker.TaskerLaunchComponentInfo 6 | 7 | class TaskerLaunchComponentHelper(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutput(config) { 8 | override val runnerClass = TaskerLaunchComponentRunner::class.java 9 | override val inputClass = TaskerLaunchComponentInfo::class.java 10 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/util/tasker/TaskerLaunchComponentRunner.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.util.tasker 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutput 6 | import com.joaomgcd.taskerpluginlibrary.input.TaskerInput 7 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult 8 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultError 9 | import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess 10 | import kotlinx.coroutines.runBlocking 11 | import tk.zwander.rootactivitylauncher.data.component.ComponentType 12 | import tk.zwander.rootactivitylauncher.data.tasker.TaskerLaunchComponentInfo 13 | import tk.zwander.rootactivitylauncher.util.findExtrasForComponent 14 | import tk.zwander.rootactivitylauncher.util.launch.launch 15 | 16 | class TaskerLaunchComponentRunner : TaskerPluginRunnerActionNoOutput() { 17 | override fun run( 18 | context: Context, 19 | input: TaskerInput 20 | ): TaskerPluginResult { 21 | val type = input.regular.type 22 | val component = input.regular.component 23 | 24 | if (type == null) { 25 | return TaskerPluginResultError(10, "The component type cannot be null!") 26 | } 27 | 28 | if (component == null) { 29 | return TaskerPluginResultError(11, "The component cannot be null!") 30 | } 31 | 32 | val componentObj = ComponentName.unflattenFromString(component) 33 | val extras = 34 | (componentObj?.let { context.findExtrasForComponent(componentObj.packageName) } ?: listOf()) + 35 | context.findExtrasForComponent(component) 36 | var result: TaskerPluginResult = TaskerPluginResultError( 37 | -1, 38 | "Unable to launch component: $type, $component. You may need root or Shizuku." 39 | ) 40 | 41 | runBlocking { 42 | val componentType = try { 43 | ComponentType.valueOf(type) 44 | } catch (_: Exception) { 45 | result = TaskerPluginResultError(12, "Invalid component type $type") 46 | null 47 | } 48 | 49 | if (context.launch( 50 | componentType ?: return@runBlocking, 51 | extras, 52 | component 53 | ).isEmpty() 54 | ) { 55 | result = TaskerPluginResultSucess() 56 | } 57 | } 58 | 59 | return result 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/ScrimView.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableFloatStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.toArgb 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.unit.dp 25 | import androidx.compose.ui.viewinterop.AndroidView 26 | import com.hmomeni.progresscircula.ProgressCircula 27 | import kotlin.math.roundToInt 28 | 29 | @Composable 30 | fun ScrimView( 31 | progress: Float?, 32 | modifier: Modifier = Modifier 33 | ) { 34 | var actualProgress by remember { 35 | mutableFloatStateOf(0f) 36 | } 37 | 38 | LaunchedEffect(progress) { 39 | if (progress != null) { 40 | actualProgress = progress 41 | } 42 | } 43 | 44 | AnimatedVisibility( 45 | visible = progress != null, 46 | modifier = modifier, 47 | enter = fadeIn(), 48 | exit = fadeOut() 49 | ) { 50 | Box( 51 | modifier = Modifier 52 | .fillMaxSize() 53 | .background(Color.Black.copy(alpha = 0.5f)) 54 | .clickable( 55 | interactionSource = remember { 56 | MutableInteractionSource() 57 | }, 58 | indication = null, 59 | ) {}, 60 | contentAlignment = Alignment.Center 61 | ) { 62 | val accentColor = MaterialTheme.colorScheme.primary 63 | val textColor = Color.White 64 | val rimWidth = with(LocalDensity.current) { 65 | 8.dp.toPx() 66 | } 67 | 68 | AndroidView( 69 | factory = { 70 | ProgressCircula(context = it).apply { 71 | indeterminate = false 72 | rimColor = accentColor.toArgb() 73 | showProgress = true 74 | speed = 0.5f 75 | this.rimWidth = rimWidth 76 | this.textColor = textColor.toArgb() 77 | } 78 | }, 79 | modifier = Modifier.size(200.dp), 80 | update = { 81 | it.progress = (actualProgress * 100).roundToInt() 82 | } 83 | ) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/AppItem.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.ElevatedCard 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.collectAsState 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.saveable.rememberSaveable 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import kotlinx.coroutines.Dispatchers 22 | import kotlinx.coroutines.withContext 23 | import tk.zwander.rootactivitylauncher.R 24 | import tk.zwander.rootactivitylauncher.data.ComponentActionButton 25 | import tk.zwander.rootactivitylauncher.data.component.BaseComponentInfo 26 | import tk.zwander.rootactivitylauncher.data.model.AppModel 27 | import tk.zwander.rootactivitylauncher.data.model.BaseInfoModel 28 | import tk.zwander.rootactivitylauncher.util.getCoilData 29 | import tk.zwander.rootactivitylauncher.util.isActuallyEnabled 30 | import tk.zwander.rootactivitylauncher.views.dialogs.ComponentInfoDialog 31 | import tk.zwander.rootactivitylauncher.views.dialogs.ExtrasDialog 32 | 33 | @SuppressLint("StateFlowValueCalledInComposition") 34 | @Composable 35 | fun AppItem( 36 | info: BaseInfoModel, 37 | isForTasker: Boolean, 38 | selectionCallback: (BaseComponentInfo) -> Unit, 39 | extractCallback: (AppModel) -> Unit, 40 | modifier: Modifier = Modifier 41 | ) { 42 | var showingIntentDialog by remember { 43 | mutableStateOf(false) 44 | } 45 | var showingComponentInfo by remember { 46 | mutableStateOf(false) 47 | } 48 | var enabled by rememberSaveable(if (info is AppModel) info.pInfo.packageName else null) { 49 | mutableStateOf(true) 50 | } 51 | 52 | val context = LocalContext.current 53 | 54 | val filteredActivities by info.filteredActivities.collectAsState() 55 | val filteredServices by info.filteredServices.collectAsState() 56 | val filteredReceivers by info.filteredReceivers.collectAsState() 57 | 58 | val activityCount by info.activitiesSize.collectAsState(info.initialActivitiesSize.value) 59 | val servicesCount by info.servicesSize.collectAsState(info.initialServicesSize.value) 60 | val receiversCount by info.receiversSize.collectAsState(info.initialReceiversSize.value) 61 | 62 | val activitiesExpanded by info.activitiesExpanded.collectAsState() 63 | val servicesExpanded by info.servicesExpanded.collectAsState() 64 | val receiversExpanded by info.receiversExpanded.collectAsState() 65 | 66 | val activitiesLoading by info.activitiesLoading.collectAsState() 67 | val servicesLoading by info.servicesLoading.collectAsState() 68 | val receiversLoading by info.receiversLoading.collectAsState() 69 | 70 | if (info is AppModel) { 71 | LaunchedEffect(info.pInfo.packageName) { 72 | enabled = withContext(Dispatchers.IO) { 73 | info.info?.isActuallyEnabled(context) == true 74 | } 75 | } 76 | } 77 | 78 | ElevatedCard( 79 | modifier = modifier, 80 | ) { 81 | Column( 82 | modifier = Modifier.fillMaxWidth(), 83 | verticalArrangement = Arrangement.spacedBy(8.dp), 84 | ) { 85 | AppBar( 86 | icon = remember(info is AppModel) { 87 | if (info is AppModel) info.info?.getCoilData() else R.drawable.baseline_favorite_24 88 | }, 89 | name = if (info is AppModel) info.label.toString() else stringResource(id = R.string.favorites), 90 | showActions = !isForTasker && info is AppModel, 91 | modifier = Modifier 92 | .fillMaxWidth() 93 | .padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), 94 | app = info, 95 | whichButtons = remember(info is AppModel) { 96 | if (info is AppModel) { 97 | listOf( 98 | ComponentActionButton.ComponentInfoButton(info.pInfo) { 99 | showingComponentInfo = true 100 | }, 101 | ComponentActionButton.IntentDialogButton(info.pInfo.packageName) { 102 | showingIntentDialog = true 103 | }, 104 | ComponentActionButton.AppInfoButton(info.pInfo.packageName), 105 | ComponentActionButton.SaveApkButton(info, extractCallback) 106 | ) 107 | } else { 108 | listOf() 109 | } 110 | }, 111 | enabled = enabled, 112 | onEnabledChanged = { 113 | enabled = it 114 | } 115 | ) 116 | 117 | ComponentGroup( 118 | titleRes = R.string.activities, 119 | items = filteredActivities, 120 | forTasker = isForTasker, 121 | expanded = activitiesExpanded, 122 | appEnabled = enabled, 123 | loading = activitiesLoading, 124 | onExpandChange = { 125 | info.activitiesExpanded.value = it 126 | }, 127 | onItemSelected = selectionCallback, 128 | modifier = Modifier.fillMaxWidth(), 129 | count = activityCount 130 | ) 131 | 132 | ComponentGroup( 133 | titleRes = R.string.services, 134 | items = filteredServices, 135 | forTasker = isForTasker, 136 | expanded = servicesExpanded, 137 | appEnabled = enabled, 138 | loading = servicesLoading, 139 | onExpandChange = { 140 | info.servicesExpanded.value = it 141 | }, 142 | onItemSelected = selectionCallback, 143 | modifier = Modifier.fillMaxWidth(), 144 | count = servicesCount 145 | ) 146 | 147 | ComponentGroup( 148 | titleRes = R.string.receivers, 149 | items = filteredReceivers, 150 | forTasker = isForTasker, 151 | expanded = receiversExpanded, 152 | appEnabled = enabled, 153 | loading = receiversLoading, 154 | onExpandChange = { 155 | info.receiversExpanded.value = it 156 | }, 157 | onItemSelected = selectionCallback, 158 | modifier = Modifier.fillMaxWidth(), 159 | count = receiversCount 160 | ) 161 | } 162 | } 163 | 164 | if (info is AppModel) { 165 | ExtrasDialog( 166 | showing = showingIntentDialog, 167 | componentKey = info.pInfo.packageName, 168 | ) { showingIntentDialog = false } 169 | 170 | ComponentInfoDialog( 171 | info = info.pInfo, 172 | showing = showingComponentInfo 173 | ) { showingComponentInfo = false } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/AppList.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState 7 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid 8 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells 9 | import androidx.compose.foundation.lazy.staggeredgrid.items 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.collectAsState 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import tk.zwander.rootactivitylauncher.data.component.BaseComponentInfo 17 | import tk.zwander.rootactivitylauncher.data.model.AppModel 18 | import tk.zwander.rootactivitylauncher.data.model.BaseInfoModel 19 | import tk.zwander.rootactivitylauncher.util.LocalFavoriteModel 20 | import tk.zwander.rootactivitylauncher.util.plus 21 | 22 | @Composable 23 | fun AppList( 24 | appListState: LazyStaggeredGridState, 25 | filteredApps: List, 26 | isForTasker: Boolean, 27 | onItemSelected: (BaseComponentInfo) -> Unit, 28 | extractCallback: (AppModel) -> Unit, 29 | modifier: Modifier = Modifier, 30 | contentPaddingValues: PaddingValues = PaddingValues(0.dp), 31 | ) { 32 | val favoriteModel = LocalFavoriteModel.current 33 | val favoriteSize by favoriteModel.totalInitialSize.collectAsState() 34 | 35 | val actualFilteredApps = remember(favoriteSize, filteredApps.toList()) { 36 | if (favoriteSize == 0) filteredApps.filterNot { it == favoriteModel } 37 | else filteredApps 38 | } 39 | 40 | LazyVerticalStaggeredGrid( 41 | columns = StaggeredGridCells.Adaptive(400.dp), 42 | modifier = modifier, 43 | contentPadding = contentPaddingValues + PaddingValues(8.dp), 44 | horizontalArrangement = Arrangement.spacedBy(8.dp), 45 | verticalItemSpacing = 8.dp, 46 | state = appListState, 47 | ) { 48 | items(items = actualFilteredApps, key = { if (it is AppModel) it.pInfo.packageName else "favorite_item" }) { info -> 49 | AppItem( 50 | info = info, 51 | isForTasker = isForTasker, 52 | selectionCallback = { 53 | onItemSelected(it) 54 | }, 55 | extractCallback = extractCallback, 56 | modifier = Modifier.fillMaxWidth() 57 | .animateItem(), 58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/ComponentGroup.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.animateContentSize 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.expandVertically 7 | import androidx.compose.animation.fadeIn 8 | import androidx.compose.animation.fadeOut 9 | import androidx.compose.animation.shrinkVertically 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.interaction.MutableInteractionSource 12 | import androidx.compose.foundation.layout.Arrangement 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.PaddingValues 15 | import androidx.compose.foundation.layout.Row 16 | import androidx.compose.foundation.layout.Spacer 17 | import androidx.compose.foundation.layout.fillMaxWidth 18 | import androidx.compose.foundation.layout.heightIn 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.foundation.layout.size 21 | import androidx.compose.foundation.lazy.LazyColumn 22 | import androidx.compose.foundation.lazy.items 23 | import androidx.compose.material.icons.Icons 24 | import androidx.compose.material.icons.filled.KeyboardArrowDown 25 | import androidx.compose.material3.CircularProgressIndicator 26 | import androidx.compose.material3.Icon 27 | import androidx.compose.material3.Text 28 | import androidx.compose.material3.ripple 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.runtime.getValue 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.ui.Alignment 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.draw.rotate 35 | import androidx.compose.ui.res.stringResource 36 | import androidx.compose.ui.unit.dp 37 | import androidx.compose.ui.unit.sp 38 | import tk.zwander.rootactivitylauncher.data.component.BaseComponentInfo 39 | 40 | @Composable 41 | fun ComponentGroup( 42 | titleRes: Int, 43 | items: List, 44 | forTasker: Boolean, 45 | expanded: Boolean, 46 | appEnabled: Boolean, 47 | loading: Boolean, 48 | onExpandChange: (Boolean) -> Unit, 49 | onItemSelected: (BaseComponentInfo) -> Unit, 50 | modifier: Modifier = Modifier, 51 | count: Int = items.size, 52 | ) { 53 | val rotation by animateFloatAsState( 54 | targetValue = if (expanded) 180f else 0f, 55 | label = "Expanded_${stringResource(titleRes, count)}", 56 | ) 57 | 58 | AnimatedVisibility(visible = count > 0) { 59 | Column( 60 | modifier = modifier 61 | ) { 62 | Row( 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .heightIn(min = 48.dp) 66 | .clickable( 67 | interactionSource = remember { 68 | MutableInteractionSource() 69 | }, 70 | indication = ripple() 71 | ) { 72 | onExpandChange(!expanded) 73 | } 74 | .padding(8.dp), 75 | verticalAlignment = Alignment.CenterVertically, 76 | horizontalArrangement = Arrangement.spacedBy(8.dp), 77 | ) { 78 | Text( 79 | text = stringResource(id = titleRes, count), 80 | fontSize = 18.sp, 81 | ) 82 | 83 | Spacer(Modifier.weight(1f)) 84 | 85 | AnimatedVisibility(visible = loading) { 86 | CircularProgressIndicator( 87 | modifier = Modifier.size(24.dp), 88 | ) 89 | } 90 | 91 | Icon( 92 | imageVector = Icons.Default.KeyboardArrowDown, 93 | contentDescription = null, 94 | modifier = Modifier.rotate(rotation), 95 | ) 96 | } 97 | 98 | AnimatedVisibility( 99 | modifier = Modifier.fillMaxWidth(), 100 | visible = expanded && items.isNotEmpty(), 101 | enter = fadeIn() + expandVertically(), 102 | exit = fadeOut() + shrinkVertically(), 103 | ) { 104 | LazyColumn( 105 | modifier = Modifier 106 | .fillMaxWidth() 107 | .heightIn(max = 500.dp) 108 | .animateContentSize(), 109 | verticalArrangement = Arrangement.spacedBy(8.dp), 110 | contentPadding = PaddingValues( 111 | top = 8.dp, 112 | bottom = 8.dp 113 | ) 114 | ) { 115 | items(items = items, key = { it.hashCode() }) { 116 | ComponentItem( 117 | forTasker = forTasker, 118 | component = it, 119 | appEnabled = appEnabled, 120 | modifier = Modifier 121 | .fillMaxWidth() 122 | .heightIn(min = 56.dp) 123 | .then(if (forTasker) { 124 | Modifier.clickable { 125 | onItemSelected(it) 126 | } 127 | } else Modifier) 128 | .padding( 129 | start = 32.dp, 130 | end = 8.dp, 131 | ) 132 | ) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/Menu.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.StringRes 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.MoreVert 8 | import androidx.compose.material3.DropdownMenu 9 | import androidx.compose.material3.DropdownMenuItem 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.DpOffset 22 | import androidx.compose.ui.unit.dp 23 | import kotlinx.parcelize.Parcelize 24 | import tk.zwander.rootactivitylauncher.R 25 | import tk.zwander.rootactivitylauncher.util.launchUrl 26 | import tk.zwander.rootactivitylauncher.views.dialogs.PatreonSupportersDialog 27 | 28 | @Parcelize 29 | private data class MenuItem( 30 | val url: String, 31 | @StringRes val nameRes: Int, 32 | ) : Parcelable 33 | 34 | @Composable 35 | fun Menu( 36 | modifier: Modifier = Modifier 37 | ) { 38 | val context = LocalContext.current 39 | val items = remember { 40 | listOf( 41 | MenuItem( 42 | "https://androiddev.social/@wander1236", 43 | R.string.mastodon 44 | ), 45 | MenuItem( 46 | "https://zwander.dev", 47 | R.string.website 48 | ), 49 | MenuItem( 50 | "https://github.com/zacharee", 51 | R.string.github 52 | ), 53 | MenuItem( 54 | "mailto:zachary@zwander.dev?subject=${ 55 | context.resources.getString(R.string.app_name) 56 | .replace(" ", "%20") 57 | }", 58 | R.string.email 59 | ), 60 | MenuItem( 61 | "https://bit.ly/ZachareeTG", 62 | R.string.telegram 63 | ), 64 | MenuItem( 65 | "https://bit.ly/zwanderDiscord", 66 | R.string.discord 67 | ), 68 | MenuItem( 69 | "https://bit.ly/zwanderPatreon", 70 | R.string.patreon 71 | ) 72 | ) 73 | } 74 | 75 | var showingMenu by remember { 76 | mutableStateOf(false) 77 | } 78 | var showingSupportersDialog by remember { 79 | mutableStateOf(false) 80 | } 81 | 82 | Box(modifier = modifier) { 83 | IconButton( 84 | onClick = { 85 | showingMenu = true 86 | } 87 | ) { 88 | Icon( 89 | imageVector = Icons.Default.MoreVert, 90 | contentDescription = stringResource(id = R.string.menu) 91 | ) 92 | } 93 | 94 | DropdownMenu( 95 | expanded = showingMenu, 96 | onDismissRequest = { showingMenu = false }, 97 | offset = DpOffset(16.dp, 0.dp), 98 | ) { 99 | items.forEach { item -> 100 | DropdownMenuItem( 101 | text = { 102 | Text(text = stringResource(id = item.nameRes)) 103 | }, 104 | onClick = { 105 | context.launchUrl(item.url) 106 | } 107 | ) 108 | } 109 | 110 | DropdownMenuItem( 111 | text = { 112 | Text(text = stringResource(id = R.string.supporters)) 113 | }, 114 | onClick = { 115 | showingSupportersDialog = true 116 | } 117 | ) 118 | } 119 | } 120 | 121 | PatreonSupportersDialog(showing = showingSupportersDialog) { 122 | showingSupportersDialog = false 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/OptionGroup.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import tk.zwander.rootactivitylauncher.data.BaseOption 16 | 17 | @Composable 18 | fun OptionGroup( 19 | name: String, 20 | modes: Array, 21 | selectedMode: T, 22 | onModeSelected: (T) -> Unit, 23 | ) { 24 | Column( 25 | modifier = Modifier.fillMaxWidth(), 26 | verticalArrangement = Arrangement.spacedBy(8.dp) 27 | ) { 28 | Text( 29 | text = name, 30 | fontSize = 18.sp, 31 | ) 32 | 33 | Column( 34 | modifier = Modifier.fillMaxWidth(), 35 | verticalArrangement = Arrangement.spacedBy(4.dp), 36 | ) { 37 | modes.forEach { mode -> 38 | SelectableCard( 39 | selected = mode.labelRes == selectedMode.labelRes, 40 | onClick = { onModeSelected(mode) }, 41 | ) { 42 | Box( 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | .heightIn(min = 48.dp), 46 | contentAlignment = Alignment.Center, 47 | ) { 48 | Text(text = stringResource(id = mode.labelRes)) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/SearchComponent.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.expandHorizontally 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.shrinkHorizontally 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 10 | import androidx.compose.foundation.layout.WindowInsets 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.isImeVisible 13 | import androidx.compose.foundation.text.KeyboardOptions 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.filled.Search 16 | import androidx.compose.material.icons.outlined.Close 17 | import androidx.compose.material3.ExperimentalMaterial3Api 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.IconButton 20 | import androidx.compose.material3.Text 21 | import androidx.compose.material3.TextField 22 | import androidx.compose.material3.TextFieldDefaults 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.LaunchedEffect 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.composed 32 | import androidx.compose.ui.focus.FocusRequester 33 | import androidx.compose.ui.focus.focusRequester 34 | import androidx.compose.ui.focus.onFocusEvent 35 | import androidx.compose.ui.graphics.Color 36 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 37 | import androidx.compose.ui.platform.LocalFocusManager 38 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 39 | import androidx.compose.ui.res.painterResource 40 | import androidx.compose.ui.res.stringResource 41 | import tk.zwander.rootactivitylauncher.R 42 | import tk.zwander.rootactivitylauncher.views.dialogs.AdvancedUsageDialog 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | @Composable 46 | fun SearchComponent( 47 | expanded: Boolean, 48 | query: String, 49 | onExpandChange: (Boolean) -> Unit, 50 | onQueryChange: (String) -> Unit, 51 | modifier: Modifier = Modifier, 52 | ) { 53 | var showingAdvancedUsage by remember { 54 | mutableStateOf(false) 55 | } 56 | 57 | val focusRequester = remember { FocusRequester() } 58 | 59 | val softwareKeyboardController = LocalSoftwareKeyboardController.current 60 | 61 | Box( 62 | modifier = modifier, 63 | contentAlignment = Alignment.CenterEnd 64 | ) { 65 | AnimatedVisibility( 66 | visible = expanded, 67 | enter = fadeIn() + expandHorizontally(expandFrom = Alignment.End), 68 | exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.End), 69 | ) { 70 | LaunchedEffect(null) { 71 | focusRequester.requestFocus() 72 | } 73 | 74 | TextField( 75 | value = query, 76 | onValueChange = onQueryChange, 77 | modifier = Modifier.fillMaxWidth() 78 | .clearFocusOnKeyboardDismiss() 79 | .focusRequester(focusRequester), 80 | colors = TextFieldDefaults.colors( 81 | focusedContainerColor = Color.Transparent, 82 | unfocusedContainerColor = Color.Transparent, 83 | ), 84 | leadingIcon = { 85 | IconButton( 86 | onClick = { 87 | showingAdvancedUsage = true 88 | } 89 | ) { 90 | Icon( 91 | painter = painterResource(id = R.drawable.ic_baseline_help_outline_24), 92 | contentDescription = stringResource(id = R.string.usage_advanced_search) 93 | ) 94 | } 95 | }, 96 | trailingIcon = { 97 | IconButton( 98 | onClick = { 99 | if (query.isNotBlank()) { 100 | onQueryChange("") 101 | } else { 102 | softwareKeyboardController?.hide() 103 | onExpandChange(false) 104 | } 105 | } 106 | ) { 107 | Icon( 108 | painter = rememberVectorPainter(Icons.Outlined.Close), 109 | contentDescription = stringResource( 110 | id = if (query.isNotBlank()) { 111 | R.string.clear 112 | } else { 113 | R.string.close 114 | } 115 | ) 116 | ) 117 | } 118 | }, 119 | keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false), 120 | placeholder = { 121 | Text(text = stringResource(R.string.search)) 122 | }, 123 | ) 124 | } 125 | 126 | AnimatedVisibility( 127 | visible = !expanded, 128 | enter = fadeIn(), 129 | exit = fadeOut() 130 | ) { 131 | IconButton(onClick = { onExpandChange(true) }) { 132 | Icon( 133 | imageVector = Icons.Default.Search, 134 | contentDescription = stringResource(id = R.string.search) 135 | ) 136 | } 137 | } 138 | } 139 | 140 | AdvancedUsageDialog(showing = showingAdvancedUsage) { 141 | showingAdvancedUsage = false 142 | } 143 | } 144 | 145 | @OptIn(ExperimentalLayoutApi::class) 146 | fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed { 147 | var isFocused by remember { mutableStateOf(false) } 148 | var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) } 149 | if (isFocused) { 150 | val imeIsVisible = WindowInsets.isImeVisible 151 | val focusManager = LocalFocusManager.current 152 | LaunchedEffect(imeIsVisible) { 153 | if (imeIsVisible) { 154 | keyboardAppearedSinceLastFocused = true 155 | } else if (keyboardAppearedSinceLastFocused) { 156 | focusManager.clearFocus() 157 | } 158 | } 159 | } 160 | onFocusEvent { 161 | if (isFocused != it.isFocused) { 162 | isFocused = it.isFocused 163 | if (isFocused) { 164 | keyboardAppearedSinceLastFocused = false 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/components/SelectableCard.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.components 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.foundation.BorderStroke 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.material3.AlertDialogDefaults 7 | import androidx.compose.material3.CardDefaults 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.OutlinedCard 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun SelectableCard( 19 | selected: Boolean, 20 | onClick: () -> Unit, 21 | modifier: Modifier = Modifier, 22 | selectedColor: Color = MaterialTheme.colorScheme.primaryContainer, 23 | unselectedColor: Color = AlertDialogDefaults.containerColor, 24 | content: @Composable ColumnScope.() -> Unit, 25 | ) { 26 | val outlineColor = MaterialTheme.colorScheme.outline 27 | 28 | val color by animateColorAsState( 29 | targetValue = if (selected) { 30 | selectedColor 31 | } else { 32 | unselectedColor 33 | }, 34 | label = "SelectableCardColor", 35 | ) 36 | 37 | OutlinedCard( 38 | modifier = modifier, 39 | colors = CardDefaults.outlinedCardColors( 40 | containerColor = color, 41 | ), 42 | onClick = onClick, 43 | content = content, 44 | border = remember { 45 | BorderStroke(1.dp, outlineColor) 46 | } 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/AdvancedUsageDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.items 9 | import androidx.compose.material3.OutlinedCard 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.TextButton 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | import tk.zwander.rootactivitylauncher.R 19 | 20 | private val items = listOf( 21 | R.string.usage_advanced_search_has_permission to R.string.usage_advanced_search_has_permission_desc, 22 | R.string.usage_advanced_search_declares_permission to R.string.usage_advanced_search_declares_permission_desc, 23 | R.string.usage_advanced_search_requires_permission to R.string.usage_advanced_search_requires_permission_desc, 24 | R.string.usage_advanced_search_requires_feature to R.string.usage_advanced_search_requires_feature_desc 25 | ) 26 | 27 | @Composable 28 | fun AdvancedUsageDialog( 29 | showing: Boolean, 30 | onDismissRequest: () -> Unit 31 | ) { 32 | if (showing) { 33 | BaseAlertDialog( 34 | onDismissRequest = onDismissRequest, 35 | title = { 36 | Text(text = stringResource(id = R.string.usage_advanced_search)) 37 | }, 38 | text = { 39 | LazyColumn( 40 | verticalArrangement = Arrangement.spacedBy(8.dp) 41 | ) { 42 | items(items = items, key = { it }) { 43 | OutlinedCard { 44 | Column( 45 | modifier = Modifier.fillMaxWidth() 46 | .padding(8.dp), 47 | verticalArrangement = Arrangement.spacedBy(8.dp) 48 | ) { 49 | Text( 50 | text = stringResource(id = it.first), 51 | fontWeight = FontWeight.Bold, 52 | fontSize = 16.sp 53 | ) 54 | 55 | Text( 56 | text = stringResource(id = it.second) 57 | ) 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | confirmButton = { 64 | TextButton(onClick = onDismissRequest) { 65 | Text(text = stringResource(id = android.R.string.ok)) 66 | } 67 | } 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/BaseAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.material3.AlertDialog 5 | import androidx.compose.material3.AlertDialogDefaults 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.Shape 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.window.DialogProperties 12 | 13 | @Composable 14 | fun BaseAlertDialog( 15 | onDismissRequest: () -> Unit, 16 | confirmButton: @Composable () -> Unit, 17 | modifier: Modifier = Modifier, 18 | dismissButton: @Composable (() -> Unit)? = null, 19 | icon: @Composable (() -> Unit)? = null, 20 | title: @Composable (() -> Unit)? = null, 21 | text: @Composable (() -> Unit)? = null, 22 | shape: Shape = AlertDialogDefaults.shape, 23 | containerColor: Color = AlertDialogDefaults.containerColor, 24 | iconContentColor: Color = AlertDialogDefaults.iconContentColor, 25 | titleContentColor: Color = AlertDialogDefaults.titleContentColor, 26 | textContentColor: Color = AlertDialogDefaults.textContentColor, 27 | tonalElevation: Dp = AlertDialogDefaults.TonalElevation, 28 | properties: DialogProperties = DialogProperties() 29 | ) { 30 | val actualProperties = DialogProperties( 31 | usePlatformDefaultWidth = false, 32 | decorFitsSystemWindows = properties.decorFitsSystemWindows, 33 | dismissOnBackPress = properties.dismissOnBackPress, 34 | dismissOnClickOutside = properties.dismissOnClickOutside, 35 | securePolicy = properties.securePolicy 36 | ) 37 | val actualModifier = modifier.fillMaxWidth(0.85f) 38 | 39 | AlertDialog( 40 | onDismissRequest = onDismissRequest, 41 | confirmButton = confirmButton, 42 | modifier = actualModifier, 43 | dismissButton = dismissButton, 44 | icon = icon, 45 | title = title, 46 | text = text, 47 | shape = shape, 48 | containerColor = containerColor, 49 | iconContentColor = iconContentColor, 50 | titleContentColor = titleContentColor, 51 | textContentColor = textContentColor, 52 | tonalElevation = tonalElevation, 53 | properties = actualProperties 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/ComponentInfoDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import android.content.ClipData 4 | import android.content.pm.ActivityInfo 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.ServiceInfo 7 | import androidx.compose.animation.Crossfade 8 | import androidx.compose.animation.animateContentSize 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.itemsIndexed 15 | import androidx.compose.foundation.text.KeyboardOptions 16 | import androidx.compose.foundation.text.selection.SelectionContainer 17 | import androidx.compose.material3.CircularProgressIndicator 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.OutlinedTextField 21 | import androidx.compose.material3.Text 22 | import androidx.compose.material3.TextButton 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.LaunchedEffect 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateListOf 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.rememberCoroutineScope 30 | import androidx.compose.runtime.setValue 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.platform.ClipEntry 34 | import androidx.compose.ui.platform.LocalClipboard 35 | import androidx.compose.ui.res.stringResource 36 | import androidx.compose.ui.text.AnnotatedString 37 | import androidx.compose.ui.unit.dp 38 | import kotlinx.coroutines.Dispatchers 39 | import kotlinx.coroutines.launch 40 | import kotlinx.coroutines.withContext 41 | import tk.zwander.rootactivitylauncher.R 42 | import tk.zwander.rootactivitylauncher.util.applyQuery 43 | import tk.zwander.rootactivitylauncher.util.processActivityInfo 44 | import tk.zwander.rootactivitylauncher.util.processPackageInfo 45 | import tk.zwander.rootactivitylauncher.util.processServiceInfo 46 | 47 | @Composable 48 | fun ComponentInfoDialog( 49 | info: T, 50 | showing: Boolean, 51 | onDismissRequest: () -> Unit, 52 | ) { 53 | if (showing) { 54 | val clipboardManager = LocalClipboard.current 55 | val highlightColor = MaterialTheme.colorScheme.primary 56 | val scope = rememberCoroutineScope() 57 | 58 | var query by remember { 59 | mutableStateOf("") 60 | } 61 | 62 | val dump = remember { 63 | mutableStateListOf() 64 | } 65 | 66 | LaunchedEffect(query) { 67 | val d = if (dump.isEmpty()) { 68 | withContext(Dispatchers.IO) { 69 | when (info) { 70 | is ActivityInfo -> processActivityInfo(info) 71 | is ServiceInfo -> processServiceInfo(info) 72 | is PackageInfo -> processPackageInfo(info) 73 | else -> listOf() 74 | } 75 | } 76 | } else dump 77 | 78 | val q = withContext(Dispatchers.IO) { 79 | applyQuery(highlightColor, d, query, dump.isEmpty()) 80 | } 81 | 82 | dump.clear() 83 | dump.addAll(q) 84 | } 85 | 86 | BaseAlertDialog( 87 | title = { 88 | Text(text = stringResource(id = R.string.component_info)) 89 | }, 90 | onDismissRequest = onDismissRequest, 91 | confirmButton = { 92 | TextButton(onClick = onDismissRequest) { 93 | Text(text = stringResource(id = android.R.string.ok)) 94 | } 95 | }, 96 | dismissButton = { 97 | TextButton( 98 | onClick = { 99 | scope.launch { 100 | clipboardManager.setClipEntry( 101 | ClipEntry(ClipData.newPlainText(null, dump.joinToString("\n"))) 102 | ) 103 | } 104 | }, 105 | ) { 106 | Text(text = stringResource(id = tk.zwander.patreonsupportersretrieval.R.string.copy)) 107 | } 108 | }, 109 | text = { 110 | ComponentInfoContents( 111 | query = query, 112 | onQueryChanged = { query = it }, 113 | content = dump, 114 | modifier = Modifier.animateContentSize() 115 | ) 116 | } 117 | ) 118 | } 119 | } 120 | 121 | @OptIn(ExperimentalMaterial3Api::class) 122 | @Composable 123 | private fun ComponentInfoContents( 124 | query: String, 125 | onQueryChanged: (String) -> Unit, 126 | content: List, 127 | modifier: Modifier = Modifier 128 | ) { 129 | Crossfade( 130 | targetState = content.isEmpty(), 131 | modifier = modifier 132 | ) { 133 | if (it) { 134 | Box( 135 | modifier = Modifier.fillMaxWidth(), 136 | contentAlignment = Alignment.Center 137 | ) { 138 | CircularProgressIndicator() 139 | } 140 | } else { 141 | Column( 142 | modifier = Modifier.fillMaxWidth(), 143 | verticalArrangement = Arrangement.spacedBy(8.dp) 144 | ) { 145 | OutlinedTextField( 146 | value = query, 147 | onValueChange = onQueryChanged, 148 | modifier = Modifier.fillMaxWidth(), 149 | label = { 150 | Text(text = stringResource(id = R.string.search)) 151 | }, 152 | keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false), 153 | ) 154 | 155 | SelectionContainer { 156 | LazyColumn( 157 | modifier = Modifier.fillMaxWidth() 158 | ) { 159 | itemsIndexed(items = content, key = { index, item -> item.toString() + index }) { _, item -> 160 | Text(text = item) 161 | } 162 | } 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/ExtraTypeDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.heightIn 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.lazy.items 9 | import androidx.compose.foundation.lazy.rememberLazyListState 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.TextButton 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.unit.dp 20 | import tk.zwander.rootactivitylauncher.R 21 | import tk.zwander.rootactivitylauncher.data.ExtraInfo 22 | import tk.zwander.rootactivitylauncher.data.ExtraType 23 | import tk.zwander.rootactivitylauncher.views.components.SelectableCard 24 | 25 | @Composable 26 | fun ExtrasTypeDialog( 27 | showing: Boolean, 28 | extraInfo: ExtraInfo, 29 | onDismissRequest: () -> Unit, 30 | onTypeSelected: (ExtraType) -> Unit, 31 | ) { 32 | if (showing) { 33 | BaseAlertDialog( 34 | onDismissRequest = { 35 | onDismissRequest() 36 | }, 37 | title = { 38 | Text(text = stringResource(id = R.string.type)) 39 | }, 40 | text = { 41 | ExtrasTypeDialogContents( 42 | initial = extraInfo.safeType, 43 | onTypeSelected = { 44 | onTypeSelected(it) 45 | onDismissRequest() 46 | }, 47 | modifier = Modifier.fillMaxWidth() 48 | ) 49 | }, 50 | confirmButton = { 51 | TextButton(onClick = { onDismissRequest() }) { 52 | Text(text = stringResource(id = android.R.string.cancel)) 53 | } 54 | }, 55 | ) 56 | } 57 | } 58 | 59 | @Composable 60 | fun ExtrasTypeDialogContents( 61 | initial: ExtraType, 62 | onTypeSelected: (ExtraType) -> Unit, 63 | modifier: Modifier = Modifier 64 | ) { 65 | val context = LocalContext.current 66 | val sortedTypes = remember { 67 | ExtraType.entries.sortedBy { 68 | context.resources.getString(it.nameRes) 69 | } 70 | } 71 | val state = rememberLazyListState() 72 | 73 | LaunchedEffect(initial) { 74 | state.scrollToItem(sortedTypes.indexOf(initial)) 75 | } 76 | 77 | LazyColumn( 78 | modifier = modifier, 79 | verticalArrangement = Arrangement.spacedBy(8.dp), 80 | state = state 81 | ) { 82 | items(items = sortedTypes, key = { it.value }) { type -> 83 | SelectableCard( 84 | modifier = Modifier.fillMaxWidth(), 85 | selected = initial == type, 86 | onClick = { 87 | onTypeSelected(type) 88 | }, 89 | ) { 90 | Box( 91 | modifier = Modifier 92 | .fillMaxWidth() 93 | .heightIn(min = 56.dp), 94 | contentAlignment = Alignment.Center, 95 | ) { 96 | Text(text = stringResource(id = type.nameRes)) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/FilterDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.rememberScrollState 7 | import androidx.compose.foundation.verticalScroll 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TextButton 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import tk.zwander.rootactivitylauncher.R 19 | import tk.zwander.rootactivitylauncher.data.FilterMode 20 | import tk.zwander.rootactivitylauncher.views.components.OptionGroup 21 | 22 | @Composable 23 | fun FilterDialog( 24 | showing: Boolean, 25 | initialEnabledMode: FilterMode.EnabledFilterMode, 26 | initialExportedMode: FilterMode.ExportedFilterMode, 27 | initialPermissionMode: FilterMode.PermissionFilterMode, 28 | initialComponentMode: FilterMode.HasComponentsFilterMode, 29 | initialSystemAppsMode: FilterMode.SystemAppFilterMode, 30 | onDismissRequest: ( 31 | FilterMode.EnabledFilterMode, 32 | FilterMode.ExportedFilterMode, 33 | FilterMode.PermissionFilterMode, 34 | FilterMode.HasComponentsFilterMode, 35 | FilterMode.SystemAppFilterMode, 36 | ) -> Unit, 37 | ) { 38 | if (showing) { 39 | var enabledMode by remember { 40 | mutableStateOf(initialEnabledMode) 41 | } 42 | var exportedMode by remember { 43 | mutableStateOf(initialExportedMode) 44 | } 45 | var permissionMode by remember { 46 | mutableStateOf(initialPermissionMode) 47 | } 48 | var componentMode by remember { 49 | mutableStateOf(initialComponentMode) 50 | } 51 | var systemAppsMode by remember { 52 | mutableStateOf(initialSystemAppsMode) 53 | } 54 | 55 | BaseAlertDialog( 56 | onDismissRequest = { 57 | onDismissRequest( 58 | initialEnabledMode, 59 | initialExportedMode, 60 | initialPermissionMode, 61 | initialComponentMode, 62 | initialSystemAppsMode, 63 | ) 64 | }, 65 | title = { 66 | Text(text = stringResource(id = R.string.filter)) 67 | }, 68 | confirmButton = { 69 | TextButton( 70 | onClick = { 71 | onDismissRequest(enabledMode, exportedMode, permissionMode, componentMode, systemAppsMode) 72 | } 73 | ) { 74 | Text(text = stringResource(id = android.R.string.ok)) 75 | } 76 | }, 77 | dismissButton = { 78 | TextButton( 79 | onClick = { 80 | onDismissRequest( 81 | initialEnabledMode, 82 | initialExportedMode, 83 | initialPermissionMode, 84 | initialComponentMode, 85 | initialSystemAppsMode, 86 | ) 87 | } 88 | ) { 89 | Text(text = stringResource(id = android.R.string.cancel)) 90 | } 91 | }, 92 | text = { 93 | Column( 94 | verticalArrangement = Arrangement.spacedBy(16.dp), 95 | modifier = Modifier 96 | .fillMaxWidth() 97 | .verticalScroll(rememberScrollState()), 98 | ) { 99 | OptionGroup( 100 | name = stringResource(id = R.string.enabled_filter), 101 | modes = arrayOf( 102 | FilterMode.EnabledFilterMode.ShowAll, 103 | FilterMode.EnabledFilterMode.ShowEnabled, 104 | FilterMode.EnabledFilterMode.ShowDisabled, 105 | ), 106 | onModeSelected = { 107 | enabledMode = it 108 | }, 109 | selectedMode = enabledMode 110 | ) 111 | 112 | OptionGroup( 113 | name = stringResource(id = R.string.exported_filter), 114 | modes = arrayOf( 115 | FilterMode.ExportedFilterMode.ShowAll, 116 | FilterMode.ExportedFilterMode.ShowExported, 117 | FilterMode.ExportedFilterMode.ShowUnexported, 118 | ), 119 | onModeSelected = { 120 | exportedMode = it 121 | }, 122 | selectedMode = exportedMode, 123 | ) 124 | 125 | OptionGroup( 126 | name = stringResource(id = R.string.permission_filter), 127 | modes = arrayOf( 128 | FilterMode.PermissionFilterMode.ShowAll, 129 | FilterMode.PermissionFilterMode.ShowRequiresPermission, 130 | FilterMode.PermissionFilterMode.ShowNoPermissionRequired, 131 | ), 132 | onModeSelected = { 133 | permissionMode = it 134 | }, 135 | selectedMode = permissionMode, 136 | ) 137 | 138 | OptionGroup( 139 | name = stringResource(id = R.string.component_filter), 140 | modes = arrayOf( 141 | FilterMode.HasComponentsFilterMode.ShowAll, 142 | FilterMode.HasComponentsFilterMode.ShowHasComponents, 143 | FilterMode.HasComponentsFilterMode.ShowHasNoComponents, 144 | ), 145 | onModeSelected = { 146 | componentMode = it 147 | }, 148 | selectedMode = componentMode, 149 | ) 150 | 151 | OptionGroup( 152 | name = stringResource(R.string.system_app_filter), 153 | modes = arrayOf( 154 | FilterMode.SystemAppFilterMode.ShowAll, 155 | FilterMode.SystemAppFilterMode.ShowSystemApps, 156 | FilterMode.SystemAppFilterMode.ShowNonSystemApps, 157 | ), 158 | onModeSelected = { 159 | systemAppsMode = it 160 | }, 161 | selectedMode = systemAppsMode, 162 | ) 163 | } 164 | }, 165 | ) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/PatreonSupportersDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import androidx.compose.animation.Crossfade 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.heightIn 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.items 12 | import androidx.compose.material3.CircularProgressIndicator 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.OutlinedCard 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TextButton 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.mutableStateListOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.unit.dp 26 | import tk.zwander.patreonsupportersretrieval.data.SupporterInfo 27 | import tk.zwander.patreonsupportersretrieval.util.DataParser 28 | import tk.zwander.rootactivitylauncher.R 29 | import tk.zwander.rootactivitylauncher.util.launchUrl 30 | 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | @Composable 33 | fun PatreonSupportersDialog( 34 | showing: Boolean, 35 | onDismissRequest: () -> Unit 36 | ) { 37 | val context = LocalContext.current 38 | val supporters = remember { 39 | mutableStateListOf() 40 | } 41 | 42 | if (showing) { 43 | LaunchedEffect(null) { 44 | supporters.clear() 45 | supporters.addAll(DataParser.getInstance(context).parseSupporters()) 46 | } 47 | 48 | BaseAlertDialog( 49 | onDismissRequest = onDismissRequest, 50 | title = { 51 | Text(text = stringResource(id = R.string.supporters)) 52 | }, 53 | confirmButton = { 54 | TextButton(onClick = onDismissRequest) { 55 | Text(text = stringResource(id = android.R.string.ok)) 56 | } 57 | }, 58 | text = { 59 | Column( 60 | horizontalAlignment = Alignment.CenterHorizontally, 61 | modifier = Modifier.fillMaxWidth() 62 | ) { 63 | Text(text = stringResource(id = tk.zwander.patreonsupportersretrieval.R.string.supporters_desc)) 64 | 65 | Spacer(modifier = Modifier.size(8.dp)) 66 | 67 | Crossfade(targetState = supporters.isEmpty()) { empty -> 68 | if (empty) { 69 | Box( 70 | modifier = Modifier.fillMaxWidth(), 71 | contentAlignment = Alignment.Center 72 | ) { 73 | CircularProgressIndicator() 74 | } 75 | } else { 76 | LazyColumn( 77 | modifier = Modifier.fillMaxWidth() 78 | ) { 79 | items(items = supporters, key = { it }) { 80 | OutlinedCard( 81 | onClick = { 82 | context.launchUrl(it.link) 83 | }, 84 | ) { 85 | Box( 86 | modifier = Modifier 87 | .fillMaxWidth() 88 | .heightIn(min = 48.dp), 89 | contentAlignment = Alignment.Center 90 | ) { 91 | Text(text = it.name) 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/dialogs/SortDialog.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.dialogs 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.rememberScrollState 7 | import androidx.compose.foundation.verticalScroll 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TextButton 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import tk.zwander.rootactivitylauncher.R 19 | import tk.zwander.rootactivitylauncher.data.SortMode 20 | import tk.zwander.rootactivitylauncher.data.SortOrder 21 | import tk.zwander.rootactivitylauncher.views.components.OptionGroup 22 | 23 | @Composable 24 | fun SortDialog( 25 | showing: Boolean, 26 | initialSortBy: SortMode, 27 | initialSortOrder: SortOrder, 28 | onDismissRequest: (SortMode, SortOrder) -> Unit, 29 | ) { 30 | if (showing) { 31 | var sortBy by remember { 32 | mutableStateOf(initialSortBy) 33 | } 34 | var sortOrder by remember { 35 | mutableStateOf(initialSortOrder) 36 | } 37 | 38 | BaseAlertDialog( 39 | onDismissRequest = { 40 | onDismissRequest(initialSortBy, initialSortOrder) 41 | }, 42 | title = { 43 | Text(text = stringResource(id = R.string.sort_apps)) 44 | }, 45 | confirmButton = { 46 | TextButton( 47 | onClick = { 48 | onDismissRequest(sortBy, sortOrder) 49 | } 50 | ) { 51 | Text(text = stringResource(id = android.R.string.ok)) 52 | } 53 | }, 54 | dismissButton = { 55 | TextButton( 56 | onClick = { 57 | onDismissRequest(initialSortBy, initialSortOrder) 58 | } 59 | ) { 60 | Text(text = stringResource(id = android.R.string.cancel)) 61 | } 62 | }, 63 | text = { 64 | Column( 65 | verticalArrangement = Arrangement.spacedBy(16.dp), 66 | modifier = Modifier 67 | .fillMaxWidth() 68 | .verticalScroll(rememberScrollState()) 69 | ) { 70 | OptionGroup( 71 | name = stringResource(id = R.string.sort_by), 72 | modes = arrayOf( 73 | SortMode.SortByName, 74 | SortMode.SortByUpdatedDate, 75 | SortMode.SortByInstalledDate, 76 | ), 77 | onModeSelected = { 78 | sortBy = it 79 | }, 80 | selectedMode = sortBy, 81 | ) 82 | 83 | OptionGroup( 84 | name = stringResource(id = R.string.sort_order), 85 | modes = arrayOf( 86 | SortOrder.Ascending, 87 | SortOrder.Descending, 88 | ), 89 | onModeSelected = { 90 | sortOrder = it 91 | }, 92 | selectedMode = sortOrder, 93 | ) 94 | } 95 | }, 96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/tk/zwander/rootactivitylauncher/views/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package tk.zwander.rootactivitylauncher.views.theme 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.dynamicDarkColorScheme 8 | import androidx.compose.material3.dynamicLightColorScheme 9 | import androidx.compose.material3.lightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.res.colorResource 13 | import tk.zwander.rootactivitylauncher.R 14 | 15 | @Composable 16 | fun Theme( 17 | dark: Boolean = isSystemInDarkTheme(), 18 | content: @Composable () -> Unit, 19 | ) { 20 | val context = LocalContext.current 21 | val isAndroid12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 22 | 23 | val colorPrimary = colorResource(R.color.colorPrimary) 24 | val colorPrimaryDark = colorResource(R.color.colorPrimaryDark) 25 | val colorAccent = colorResource(R.color.colorAccent) 26 | 27 | MaterialTheme( 28 | colorScheme = if (dark) { 29 | if (isAndroid12) dynamicDarkColorScheme(context) else darkColorScheme( 30 | primary = colorPrimary, 31 | secondary = colorAccent, 32 | tertiary = colorPrimaryDark, 33 | ) 34 | } else { 35 | if (isAndroid12) dynamicLightColorScheme(context) else lightColorScheme( 36 | primary = colorPrimary, 37 | secondary = colorAccent, 38 | tertiary = colorPrimaryDark, 39 | ) 40 | }, 41 | content = content, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_filter_list_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_help_outline_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_link_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_open_in_new_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/save.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scroll_to_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scroll_to_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sort.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tune.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_neutral1_800 4 | @android:color/system_accent1_100 5 | @color/iconBackgroundStart 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #F44336 4 | #D32F2F 5 | #7C4DFF 6 | 7 | #00ff00 8 | #ffff00 9 | #ff0000 10 | 11 | #FFFFFF 12 | @color/colorPrimaryDark 13 | @color/colorAccent 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/xml/device_admin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.kotlin.android) apply false 4 | alias(libs.plugins.kotlin.parcelize) apply false 5 | alias(libs.plugins.kotlin.atomicfu) apply false 6 | alias(libs.plugins.kotlin.compose) apply false 7 | alias(libs.plugins.bugsnag.android) apply false 8 | } 9 | 10 | tasks.register("clean") { 11 | delete(rootProject.layout.buildDirectory) 12 | } 13 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | kotlinx.atomicfu.enableJvmIrTransformation=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | activityCompose = "1.10.1" 3 | androidGradlePlugin = "8.10.1" 4 | appcompat = "1.7.0" 5 | atomicfu = "0.27.0" 6 | bugsnagAndroid = "6.13.0" 7 | bugsnagAndroidGradle = "8.1.0" 8 | coil = "2.7.0" 9 | compose = "1.8.2" 10 | composeThemeAdapter3 = "1.1.1" 11 | composetooltip = "0.2.0" 12 | coreKtx = "1.16.0" 13 | datastorePreferences = "1.1.7" 14 | dhizukuApi = "2.5.3" 15 | gson = "2.12.1" 16 | hiddenapibypass = "6.1" 17 | kotlin = "2.1.21" 18 | kotlinxCoroutines = "1.10.2" 19 | libsuperuser = "1.1.1" 20 | material3 = "1.3.2" 21 | materialVersion = "1.12.0" 22 | patreonsupportersretrieval = "8436bbdbdc" 23 | preferenceKtx = "1.2.1" 24 | progresscircula = "0.2.1" 25 | shizuku = "13.1.5" 26 | taskerpluginlibrary = "0.4.10" 27 | 28 | [libraries] 29 | activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } 30 | appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } 31 | atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } 32 | bugsnag-android = { module = "com.bugsnag:bugsnag-android", version.ref = "bugsnagAndroid" } 33 | coil = { module = "io.coil-kt:coil", version.ref = "coil" } 34 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } 35 | compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } 36 | compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } 37 | compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } 38 | compose-theme-adapter3 = { module = "com.google.android.material:compose-theme-adapter-3", version.ref = "composeThemeAdapter3" } 39 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 40 | composetooltip = { module = "com.github.skgmn:composetooltip", version.ref = "composetooltip" } 41 | core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } 42 | datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } 43 | dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizukuApi" } 44 | google-material = { module = "com.google.android.material:material", version.ref = "materialVersion" } 45 | gson = { module = "com.google.code.gson:gson", version.ref = "gson" } 46 | hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" } 47 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 48 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 49 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 50 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 51 | libsuperuser = { module = "com.github.Chainfire:libsuperuser", version.ref = "libsuperuser" } 52 | patreonSupportersRetrieval = { module = "com.github.zacharee:PatreonSupportersRetrieval", version.ref = "patreonsupportersretrieval" } 53 | preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } 54 | progressCircula = { module = "com.github.2hamed:ProgressCircula", version.ref = "progresscircula" } 55 | shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } 56 | shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } 57 | taskerpluginlibrary = { module = "com.joaomgcd:taskerpluginlibrary", version.ref = "taskerpluginlibrary" } 58 | 59 | [plugins] 60 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 61 | bugsnag-android = { id = "com.bugsnag.android.gradle", version.ref = "bugsnagAndroidGradle" } 62 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 63 | kotlin-atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } 64 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 65 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 66 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharee/RootActivityLauncher/58c903ddabdf33bab83c2c730dd06a19ad9088a1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 28 23:03:07 EDT 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | google { 6 | content { 7 | includeGroupByRegex("com\\.android.*") 8 | includeGroupByRegex("com\\.google.*") 9 | includeGroupByRegex("androidx.*") 10 | } 11 | } 12 | mavenCentral() 13 | gradlePluginPortal() 14 | } 15 | } 16 | dependencyResolutionManagement { 17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 18 | repositories { 19 | google() 20 | mavenCentral() 21 | maven(url = "https://jitpack.io") 22 | } 23 | } 24 | 25 | include(":app") 26 | rootProject.name = "Root Activity Launcher" --------------------------------------------------------------------------------