Merge pull request #995 from ManiaciaChao/feat/custom-legacy-installer

Add support for specifying custom installer (`LegacyInstaller` only)
This commit is contained in:
LooKeR 2025-05-31 18:22:49 +05:30 committed by GitHub
commit 582d526faf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 217 additions and 21 deletions

View File

@ -14,6 +14,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey
import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.datastore.model.LegacyInstallerComponent
import com.looker.droidify.datastore.model.ProxyPreference import com.looker.droidify.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.datastore.model.SortOrder
@ -85,6 +86,31 @@ class PreferenceSettingsRepository(
override suspend fun setInstallerType(installerType: InstallerType) = override suspend fun setInstallerType(installerType: InstallerType) =
INSTALLER_TYPE.update(installerType.name) INSTALLER_TYPE.update(installerType.name)
override suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?) {
when (component) {
null -> {
LEGACY_INSTALLER_COMPONENT_TYPE.update("")
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
}
is LegacyInstallerComponent.Component -> {
LEGACY_INSTALLER_COMPONENT_TYPE.update("component")
LEGACY_INSTALLER_COMPONENT_CLASS.update(component.clazz)
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update(component.activity)
}
LegacyInstallerComponent.Unspecified -> {
LEGACY_INSTALLER_COMPONENT_TYPE.update("unspecified")
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
}
LegacyInstallerComponent.AlwaysChoose -> {
LEGACY_INSTALLER_COMPONENT_TYPE.update("always_choose")
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
}
}
}
override suspend fun setAutoUpdate(allow: Boolean) = override suspend fun setAutoUpdate(allow: Boolean) =
AUTO_UPDATE.update(allow) AUTO_UPDATE.update(allow)
@ -125,6 +151,18 @@ class PreferenceSettingsRepository(
private fun mapSettings(preferences: Preferences): Settings { private fun mapSettings(preferences: Preferences): Settings {
val installerType = val installerType =
InstallerType.valueOf(preferences[INSTALLER_TYPE] ?: InstallerType.Default.name) InstallerType.valueOf(preferences[INSTALLER_TYPE] ?: InstallerType.Default.name)
val legacyInstallerComponent = when (preferences[LEGACY_INSTALLER_COMPONENT_TYPE]) {
"component" -> {
preferences[LEGACY_INSTALLER_COMPONENT_CLASS]?.takeIf { it.isNotBlank() }?.let { cls ->
preferences[LEGACY_INSTALLER_COMPONENT_ACTIVITY]?.takeIf { it.isNotBlank() }?.let { act ->
LegacyInstallerComponent.Component(cls, act)
}
}
}
"unspecified" -> LegacyInstallerComponent.Unspecified
"always_choose" -> LegacyInstallerComponent.AlwaysChoose
else -> null
}
val language = preferences[LANGUAGE] ?: "system" val language = preferences[LANGUAGE] ?: "system"
val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false
@ -154,6 +192,7 @@ class PreferenceSettingsRepository(
theme = theme, theme = theme,
dynamicTheme = dynamicTheme, dynamicTheme = dynamicTheme,
installerType = installerType, installerType = installerType,
legacyInstallerComponent = legacyInstallerComponent,
autoUpdate = autoUpdate, autoUpdate = autoUpdate,
autoSync = autoSync, autoSync = autoSync,
sortOrder = sortOrder, sortOrder = sortOrder,
@ -185,6 +224,9 @@ class PreferenceSettingsRepository(
val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time") val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time")
val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps") val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps")
val HOME_SCREEN_SWIPING = booleanPreferencesKey("key_home_swiping") val HOME_SCREEN_SWIPING = booleanPreferencesKey("key_home_swiping")
val LEGACY_INSTALLER_COMPONENT_CLASS = stringPreferencesKey("key_legacy_installer_component_class")
val LEGACY_INSTALLER_COMPONENT_ACTIVITY = stringPreferencesKey("key_legacy_installer_component_activity")
val LEGACY_INSTALLER_COMPONENT_TYPE = stringPreferencesKey("key_legacy_installer_component_type")
// Enums // Enums
val THEME = stringPreferencesKey("key_theme") val THEME = stringPreferencesKey("key_theme")
@ -200,6 +242,28 @@ class PreferenceSettingsRepository(
set(UNSTABLE_UPDATES, settings.unstableUpdate) set(UNSTABLE_UPDATES, settings.unstableUpdate)
set(THEME, settings.theme.name) set(THEME, settings.theme.name)
set(DYNAMIC_THEME, settings.dynamicTheme) set(DYNAMIC_THEME, settings.dynamicTheme)
when (settings.legacyInstallerComponent) {
is LegacyInstallerComponent.Component -> {
set(LEGACY_INSTALLER_COMPONENT_TYPE, "component")
set(LEGACY_INSTALLER_COMPONENT_CLASS, settings.legacyInstallerComponent.clazz)
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, settings.legacyInstallerComponent.activity)
}
LegacyInstallerComponent.Unspecified -> {
set(LEGACY_INSTALLER_COMPONENT_TYPE, "unspecified")
set(LEGACY_INSTALLER_COMPONENT_CLASS, "")
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "")
}
LegacyInstallerComponent.AlwaysChoose -> {
set(LEGACY_INSTALLER_COMPONENT_TYPE, "always_choose")
set(LEGACY_INSTALLER_COMPONENT_CLASS, "")
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "")
}
null -> {
set(LEGACY_INSTALLER_COMPONENT_TYPE, "")
set(LEGACY_INSTALLER_COMPONENT_CLASS, "")
set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, "")
}
}
set(INSTALLER_TYPE, settings.installerType.name) set(INSTALLER_TYPE, settings.installerType.name)
set(AUTO_UPDATE, settings.autoUpdate) set(AUTO_UPDATE, settings.autoUpdate)
set(AUTO_SYNC, settings.autoSync.name) set(AUTO_SYNC, settings.autoSync.name)

View File

@ -3,6 +3,7 @@ package com.looker.droidify.datastore
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.datastore.model.LegacyInstallerComponent
import com.looker.droidify.datastore.model.ProxyPreference import com.looker.droidify.datastore.model.ProxyPreference
import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.datastore.model.SortOrder
import com.looker.droidify.datastore.model.Theme import com.looker.droidify.datastore.model.Theme
@ -29,6 +30,7 @@ data class Settings(
val theme: Theme = Theme.SYSTEM, val theme: Theme = Theme.SYSTEM,
val dynamicTheme: Boolean = false, val dynamicTheme: Boolean = false,
val installerType: InstallerType = InstallerType.Default, val installerType: InstallerType = InstallerType.Default,
val legacyInstallerComponent: LegacyInstallerComponent? = null,
val autoUpdate: Boolean = false, val autoUpdate: Boolean = false,
val autoSync: AutoSync = AutoSync.WIFI_ONLY, val autoSync: AutoSync = AutoSync.WIFI_ONLY,
val sortOrder: SortOrder = SortOrder.UPDATED, val sortOrder: SortOrder = SortOrder.UPDATED,

View File

@ -3,6 +3,7 @@ package com.looker.droidify.datastore
import android.net.Uri import android.net.Uri
import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.datastore.model.LegacyInstallerComponent
import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.datastore.model.SortOrder
import com.looker.droidify.datastore.model.Theme import com.looker.droidify.datastore.model.Theme
@ -37,6 +38,8 @@ interface SettingsRepository {
suspend fun setInstallerType(installerType: InstallerType) suspend fun setInstallerType(installerType: InstallerType)
suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?)
suspend fun setAutoUpdate(allow: Boolean) suspend fun setAutoUpdate(allow: Boolean)
suspend fun setAutoSync(autoSync: AutoSync) suspend fun setAutoSync(autoSync: AutoSync)

View File

@ -0,0 +1,26 @@
package com.looker.droidify.datastore.model
import kotlinx.serialization.Serializable
@Serializable
sealed class LegacyInstallerComponent {
@Serializable
object Unspecified : LegacyInstallerComponent()
@Serializable
object AlwaysChoose : LegacyInstallerComponent()
@Serializable
data class Component(
val clazz: String,
val activity: String,
) : LegacyInstallerComponent() {
fun update(
newClazz: String? = null,
newActivity: String? = null,
): Component = copy(
clazz = newClazz ?: clazz,
activity = newActivity ?: activity
)
}
}

View File

@ -32,7 +32,7 @@ import kotlinx.coroutines.sync.withLock
class InstallManager( class InstallManager(
private val context: Context, private val context: Context,
settingsRepository: SettingsRepository private val settingsRepository: SettingsRepository
) { ) {
private val installItems = Channel<InstallItem>() private val installItems = Channel<InstallItem>()
@ -115,7 +115,7 @@ class InstallManager(
private suspend fun setInstaller(installerType: InstallerType) { private suspend fun setInstaller(installerType: InstallerType) {
lock.withLock { lock.withLock {
_installer = when (installerType) { _installer = when (installerType) {
InstallerType.LEGACY -> LegacyInstaller(context) InstallerType.LEGACY -> LegacyInstaller(context, settingsRepository)
InstallerType.SESSION -> SessionInstaller(context) InstallerType.SESSION -> SessionInstaller(context)
InstallerType.SHIZUKU -> ShizukuInstaller(context) InstallerType.SHIZUKU -> ShizukuInstaller(context)
InstallerType.ROOT -> RootInstaller(context) InstallerType.ROOT -> RootInstaller(context)

View File

@ -1,20 +1,29 @@
package com.looker.droidify.installer.installers package com.looker.droidify.installer.installers
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.AndroidRuntimeException import android.util.AndroidRuntimeException
import androidx.core.net.toUri import androidx.core.net.toUri
import com.looker.droidify.R
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.get
import com.looker.droidify.datastore.model.LegacyInstallerComponent
import com.looker.droidify.domain.model.PackageName import com.looker.droidify.domain.model.PackageName
import com.looker.droidify.installer.model.InstallItem import com.looker.droidify.installer.model.InstallItem
import com.looker.droidify.installer.model.InstallState import com.looker.droidify.installer.model.InstallState
import com.looker.droidify.utility.common.SdkCheck import com.looker.droidify.utility.common.SdkCheck
import com.looker.droidify.utility.common.cache.Cache import com.looker.droidify.utility.common.cache.Cache
import com.looker.droidify.utility.common.extension.intent import com.looker.droidify.utility.common.extension.intent
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class LegacyInstaller(private val context: Context) : Installer { class LegacyInstaller(
private val context: Context,
private val settingsRepository: SettingsRepository
) : Installer {
companion object { companion object {
private const val APK_MIME = "application/vnd.android.package-archive" private const val APK_MIME = "application/vnd.android.package-archive"
@ -22,29 +31,50 @@ class LegacyInstaller(private val context: Context) : Installer {
override suspend fun install( override suspend fun install(
installItem: InstallItem, installItem: InstallItem,
): InstallState = suspendCancellableCoroutine { cont -> ): InstallState {
val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0 val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0
val fileUri = if (SdkCheck.isNougat) { val fileUri = if (SdkCheck.isNougat) {
Cache.getReleaseUri( Cache.getReleaseUri(context, installItem.installFileName)
context,
installItem.installFileName
)
} else { } else {
Cache.getReleaseFile(context, installItem.installFileName).toUri() Cache.getReleaseFile(context, installItem.installFileName).toUri()
} }
val installIntent = intent(Intent.ACTION_INSTALL_PACKAGE) {
setDataAndType(fileUri, APK_MIME) val comp = settingsRepository.get { legacyInstallerComponent }.firstOrNull()
flags = installFlag
} return suspendCancellableCoroutine { cont ->
try { val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
context.startActivity(installIntent) setDataAndType(fileUri, APK_MIME)
cont.resume(InstallState.Installed) flags = installFlag
} catch (e: AndroidRuntimeException) { when (comp) {
installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK is LegacyInstallerComponent.Component -> {
context.startActivity(installIntent) component = ComponentName(comp.clazz, comp.activity)
cont.resume(InstallState.Installed) }
} catch (e: Exception) { else -> {
cont.resume(InstallState.Failed) // For Unspecified and AlwaysChoose, don't set component
}
}
}
val installIntent = when (comp) {
LegacyInstallerComponent.AlwaysChoose -> Intent.createChooser(intent, context.getString(
R.string.select_installer))
else -> intent
}
try {
context.startActivity(installIntent)
cont.resume(InstallState.Installed)
} catch (e: AndroidRuntimeException) {
installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK
try {
context.startActivity(installIntent)
cont.resume(InstallState.Installed)
} catch (e: Exception) {
cont.resume(InstallState.Failed)
}
} catch (e: Exception) {
cont.resume(InstallState.Failed)
}
} }
} }

View File

@ -2,6 +2,7 @@ package com.looker.droidify.ui.settings
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -36,6 +37,7 @@ import com.looker.droidify.datastore.extension.themeName
import com.looker.droidify.datastore.extension.toTime import com.looker.droidify.datastore.extension.toTime
import com.looker.droidify.datastore.model.AutoSync import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.datastore.model.LegacyInstallerComponent
import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.datastore.model.Theme import com.looker.droidify.datastore.model.Theme
import com.looker.droidify.utility.common.SdkCheck import com.looker.droidify.utility.common.SdkCheck
@ -53,6 +55,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import com.google.android.material.R as MaterialR import com.google.android.material.R as MaterialR
import androidx.core.net.toUri
@AndroidEntryPoint @AndroidEntryPoint
class SettingsFragment : Fragment() { class SettingsFragment : Fragment() {
@ -230,6 +233,56 @@ class SettingsFragment : Fragment() {
onClick = { viewModel.setInstaller(requireContext(), it) } onClick = { viewModel.setInstaller(requireContext(), it) }
) )
} }
val pm = requireContext().packageManager
legacyInstallerComponent.connect(
titleText = getString(R.string.legacyInstallerComponent),
setting = viewModel.getSetting { legacyInstallerComponent },
map = {
when (it) {
is LegacyInstallerComponent.Component -> {
val component = it
val appLabel = runCatching {
val info = pm.getApplicationInfo(component.clazz, 0)
pm.getApplicationLabel(info).toString()
}.getOrElse { component.clazz }
"$appLabel (${component.activity})"
}
LegacyInstallerComponent.Unspecified -> getString(R.string.unspecified)
LegacyInstallerComponent.AlwaysChoose -> getString(R.string.always_choose)
null -> getString(R.string.unspecified)
}
},
) { component, valueToString ->
val installerOptions = run {
var contentProtocol = "content://"
val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
setDataAndType(contentProtocol.toUri(), "application/vnd.android.package-archive")
}
val activities = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
listOf(
LegacyInstallerComponent.Unspecified,
LegacyInstallerComponent.AlwaysChoose
) + activities.map {
LegacyInstallerComponent.Component(
clazz = it.activityInfo.packageName,
activity = it.activityInfo.name,
)
}
}
addSingleCorrectDialog(
initialValue = component ?: LegacyInstallerComponent.Unspecified,
values = installerOptions,
title = R.string.legacyInstallerComponent,
iconRes = R.drawable.ic_apk_install,
valueToString = valueToString,
onClick = { viewModel.setLegacyInstallerComponentComponent(it) },
)
}
incompatibleUpdates.connect(
titleText = getString(R.string.incompatible_versions),
contentText = getString(R.string.incompatible_versions_summary),
setting = viewModel.getInitialSetting { incompatibleVersions },
)
proxyType.connect( proxyType.connect(
titleText = getString(R.string.proxy_type), titleText = getString(R.string.proxy_type),
setting = viewModel.getSetting { proxy.type }, setting = viewModel.getSetting { proxy.type },
@ -389,6 +442,9 @@ class SettingsFragment : Fragment() {
proxyHost.root.isVisible = allowProxies proxyHost.root.isVisible = allowProxies
proxyPort.root.isVisible = allowProxies proxyPort.root.isVisible = allowProxies
forceCleanUp.root.isVisible = settings.cleanUpInterval == Duration.INFINITE forceCleanUp.root.isVisible = settings.cleanUpInterval == Duration.INFINITE
val useLegacyInstaller = settings.installerType == InstallerType.LEGACY
legacyInstallerComponent.root.isVisible = useLegacyInstaller
} }
} }

View File

@ -17,6 +17,7 @@ import com.looker.droidify.datastore.model.AutoSync
import com.looker.droidify.datastore.model.InstallerType import com.looker.droidify.datastore.model.InstallerType
import com.looker.droidify.datastore.model.InstallerType.ROOT import com.looker.droidify.datastore.model.InstallerType.ROOT
import com.looker.droidify.datastore.model.InstallerType.SHIZUKU import com.looker.droidify.datastore.model.InstallerType.SHIZUKU
import com.looker.droidify.datastore.model.LegacyInstallerComponent
import com.looker.droidify.datastore.model.ProxyType import com.looker.droidify.datastore.model.ProxyType
import com.looker.droidify.datastore.model.Theme import com.looker.droidify.datastore.model.Theme
import com.looker.droidify.installer.installers.isMagiskGranted import com.looker.droidify.installer.installers.isMagiskGranted
@ -191,6 +192,12 @@ class SettingsViewModel
} }
} }
fun setLegacyInstallerComponentComponent(component: LegacyInstallerComponent?) {
viewModelScope.launch {
settingsRepository.setLegacyInstallerComponent(component)
}
}
fun exportSettings(file: Uri) { fun exportSettings(file: Uri) {
viewModelScope.launch { viewModelScope.launch {
settingsRepository.export(file) settingsRepository.export(file)

View File

@ -145,6 +145,9 @@
<include <include
android:id="@+id/installer" android:id="@+id/installer"
layout="@layout/enum_type" /> layout="@layout/enum_type" />
<include
android:id="@+id/legacy_installer_component"
layout="@layout/enum_type" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -103,12 +103,15 @@
<string name="install">Install</string> <string name="install">Install</string>
<string name="install_types">Installation Types</string> <string name="install_types">Installation Types</string>
<string name="installer">Installer</string> <string name="installer">Installer</string>
<string name="legacyInstallerComponent">Legacy Installer Component</string>
<string name="unspecified">Unspecified</string>
<string name="insufficient_storage">Insufficient Space</string> <string name="insufficient_storage">Insufficient Space</string>
<string name="insufficient_storage_DESC">There is not enough free space on the device to install this application. Try clearing some space</string> <string name="insufficient_storage_DESC">There is not enough free space on the device to install this application. Try clearing some space</string>
<string name="legacy_installer">Legacy Installer</string> <string name="legacy_installer">Legacy Installer</string>
<string name="session_installer">Session Installer</string> <string name="session_installer">Session Installer</string>
<string name="root_installer">Root Installer</string> <string name="root_installer">Root Installer</string>
<string name="shizuku_installer">Shizuku Installer</string> <string name="shizuku_installer">Shizuku Installer</string>
<string name="shizuku_legacy_installer">Shizuku Legacy Installer</string>
<string name="shizuku_not_alive">Shizuku is not running</string> <string name="shizuku_not_alive">Shizuku is not running</string>
<string name="shizuku_not_installed">Shizuku is not installed</string> <string name="shizuku_not_installed">Shizuku is not installed</string>
<string name="installed">Installed</string> <string name="installed">Installed</string>
@ -238,4 +241,6 @@
<string name="label_open_video">Video</string> <string name="label_open_video">Video</string>
<string name="label_targets_sdk">Targets: Android %s</string> <string name="label_targets_sdk">Targets: Android %s</string>
<string name="label_unknown_sdk">Unknown (%d)</string> <string name="label_unknown_sdk">Unknown (%d)</string>
<string name="always_choose">Always Choose</string>
<string name="select_installer">Select installer</string>
</resources> </resources>