feat: allow specify custom installer for LegacyInstaller

Useful for customizing installation behavior.
This commit is contained in:
maniacata 2025-05-22 01:18:51 +08:00
parent 2e5a61d136
commit 815aebe28c
10 changed files with 138 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,16 @@ 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?) {
if (component == null) {
LEGACY_INSTALLER_COMPONENT_CLASS.update("")
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update("")
return
}
LEGACY_INSTALLER_COMPONENT_CLASS.update(component.clazz)
LEGACY_INSTALLER_COMPONENT_ACTIVITY.update(component.activity)
}
override suspend fun setAutoUpdate(allow: Boolean) = override suspend fun setAutoUpdate(allow: Boolean) =
AUTO_UPDATE.update(allow) AUTO_UPDATE.update(allow)
@ -125,6 +136,12 @@ 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 =
preferences[LEGACY_INSTALLER_COMPONENT_CLASS]?.takeIf { it.isNotBlank() }?.let { cls ->
preferences[LEGACY_INSTALLER_COMPONENT_ACTIVITY]?.takeIf { it.isNotBlank() }?.let { act ->
LegacyInstallerComponent(cls, act)
}
}
val language = preferences[LANGUAGE] ?: "system" val language = preferences[LANGUAGE] ?: "system"
val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false val incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false
@ -154,6 +171,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 +203,8 @@ 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")
// Enums // Enums
val THEME = stringPreferencesKey("key_theme") val THEME = stringPreferencesKey("key_theme")
@ -200,6 +220,8 @@ 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)
settings.legacyInstallerComponent?.let { set(LEGACY_INSTALLER_COMPONENT_CLASS, it.clazz) }
settings.legacyInstallerComponent?.let { set(LEGACY_INSTALLER_COMPONENT_ACTIVITY, it.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,17 @@
package com.looker.droidify.datastore.model
import kotlinx.serialization.Serializable
@Serializable
data class LegacyInstallerComponent(
val clazz: String,
val activity: String,
) {
fun update(
newClazz: String? = null,
newActivity: String? = null,
): LegacyInstallerComponent = 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,25 @@
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.datastore.SettingsRepository
import com.looker.droidify.datastore.get
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,30 +27,38 @@ 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) {
val comp = settingsRepository.get { legacyInstallerComponent }.firstOrNull()
return suspendCancellableCoroutine { cont ->
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
setDataAndType(fileUri, APK_MIME) setDataAndType(fileUri, APK_MIME)
flags = installFlag flags = installFlag
component = comp?.let { ComponentName(it.clazz, it.activity) }
} }
try { try {
context.startActivity(installIntent) context.startActivity(installIntent)
cont.resume(InstallState.Installed) cont.resume(InstallState.Installed)
} catch (e: AndroidRuntimeException) { } catch (e: AndroidRuntimeException) {
installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK installIntent.flags = installFlag or Intent.FLAG_ACTIVITY_NEW_TASK
try {
context.startActivity(installIntent) context.startActivity(installIntent)
cont.resume(InstallState.Installed) cont.resume(InstallState.Installed)
} catch (e: Exception) { } catch (e: Exception) {
cont.resume(InstallState.Failed) cont.resume(InstallState.Failed)
} }
} catch (e: Exception) {
cont.resume(InstallState.Failed)
}
}
} }
override suspend fun uninstall(packageName: PackageName) = override suspend fun uninstall(packageName: PackageName) =

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,47 @@ 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 = {
it?.let { component ->
val appLabel = runCatching {
val info = pm.getApplicationInfo(component.clazz, 0)
pm.getApplicationLabel(info).toString()
}.getOrElse { component.clazz }
"$appLabel (${component.activity})"
} ?: 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?>(null) + activities.map {
LegacyInstallerComponent(
clazz = it.activityInfo.packageName,
activity = it.activityInfo.name,
)
}
}
addSingleCorrectDialog(
initialValue = component,
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 +433,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>