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 com.looker.droidify.datastore.model.AutoSync
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.ProxyType
import com.looker.droidify.datastore.model.SortOrder
@ -85,6 +86,16 @@ class PreferenceSettingsRepository(
override suspend fun setInstallerType(installerType: InstallerType) =
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) =
AUTO_UPDATE.update(allow)
@ -125,6 +136,12 @@ class PreferenceSettingsRepository(
private fun mapSettings(preferences: Preferences): Settings {
val installerType =
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 incompatibleVersions = preferences[INCOMPATIBLE_VERSIONS] ?: false
@ -154,6 +171,7 @@ class PreferenceSettingsRepository(
theme = theme,
dynamicTheme = dynamicTheme,
installerType = installerType,
legacyInstallerComponent = legacyInstallerComponent,
autoUpdate = autoUpdate,
autoSync = autoSync,
sortOrder = sortOrder,
@ -185,6 +203,8 @@ class PreferenceSettingsRepository(
val LAST_CLEAN_UP = longPreferencesKey("key_last_clean_up_time")
val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps")
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
val THEME = stringPreferencesKey("key_theme")
@ -200,6 +220,8 @@ class PreferenceSettingsRepository(
set(UNSTABLE_UPDATES, settings.unstableUpdate)
set(THEME, settings.theme.name)
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(AUTO_UPDATE, settings.autoUpdate)
set(AUTO_SYNC, settings.autoSync.name)

View File

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

View File

@ -3,6 +3,7 @@ package com.looker.droidify.datastore
import android.net.Uri
import com.looker.droidify.datastore.model.AutoSync
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.SortOrder
import com.looker.droidify.datastore.model.Theme
@ -37,6 +38,8 @@ interface SettingsRepository {
suspend fun setInstallerType(installerType: InstallerType)
suspend fun setLegacyInstallerComponent(component: LegacyInstallerComponent?)
suspend fun setAutoUpdate(allow: Boolean)
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(
private val context: Context,
settingsRepository: SettingsRepository
private val settingsRepository: SettingsRepository
) {
private val installItems = Channel<InstallItem>()
@ -115,7 +115,7 @@ class InstallManager(
private suspend fun setInstaller(installerType: InstallerType) {
lock.withLock {
_installer = when (installerType) {
InstallerType.LEGACY -> LegacyInstaller(context)
InstallerType.LEGACY -> LegacyInstaller(context, settingsRepository)
InstallerType.SESSION -> SessionInstaller(context)
InstallerType.SHIZUKU -> ShizukuInstaller(context)
InstallerType.ROOT -> RootInstaller(context)

View File

@ -1,20 +1,25 @@
package com.looker.droidify.installer.installers
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.util.AndroidRuntimeException
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.installer.model.InstallItem
import com.looker.droidify.installer.model.InstallState
import com.looker.droidify.utility.common.SdkCheck
import com.looker.droidify.utility.common.cache.Cache
import com.looker.droidify.utility.common.extension.intent
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
@Suppress("DEPRECATION")
class LegacyInstaller(private val context: Context) : Installer {
class LegacyInstaller(private val context: Context,
private val settingsRepository: SettingsRepository) : Installer {
companion object {
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(
installItem: InstallItem,
): InstallState = suspendCancellableCoroutine { cont ->
): InstallState {
val installFlag = if (SdkCheck.isNougat) Intent.FLAG_GRANT_READ_URI_PERMISSION else 0
val fileUri = if (SdkCheck.isNougat) {
Cache.getReleaseUri(
context,
installItem.installFileName
)
Cache.getReleaseUri(context, installItem.installFileName)
} else {
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)
flags = installFlag
component = comp?.let { ComponentName(it.clazz, it.activity) }
}
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)
}
}
}
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.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
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.model.AutoSync
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.Theme
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.hours
import com.google.android.material.R as MaterialR
import androidx.core.net.toUri
@AndroidEntryPoint
class SettingsFragment : Fragment() {
@ -230,6 +233,47 @@ class SettingsFragment : Fragment() {
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(
titleText = getString(R.string.proxy_type),
setting = viewModel.getSetting { proxy.type },
@ -389,6 +433,9 @@ class SettingsFragment : Fragment() {
proxyHost.root.isVisible = allowProxies
proxyPort.root.isVisible = allowProxies
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.ROOT
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.Theme
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) {
viewModelScope.launch {
settingsRepository.export(file)

View File

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

View File

@ -103,12 +103,15 @@
<string name="install">Install</string>
<string name="install_types">Installation Types</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_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="session_installer">Session Installer</string>
<string name="root_installer">Root 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_installed">Shizuku is not installed</string>
<string name="installed">Installed</string>