diff --git a/app/schemas/dev.sanmer.pi.database.AppDatabase/2.json b/app/schemas/dev.sanmer.pi.database.AppDatabase/2.json new file mode 100644 index 00000000..70a575a1 --- /dev/null +++ b/app/schemas/dev.sanmer.pi.database.AppDatabase/2.json @@ -0,0 +1,66 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "2f898d36eac649a9827b70b7bb6d234b", + "entities": [ + { + "tableName": "packages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `authorized` INTEGER NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorized", + "columnName": "authorized", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2f898d36eac649a9827b70b7bb6d234b')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/app/Const.kt b/app/src/main/kotlin/dev/sanmer/pi/app/Const.kt index 903069e1..cbc31b6b 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/app/Const.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/app/Const.kt @@ -8,4 +8,9 @@ object Const { object MIME { const val APK = "application/vnd.android.package-archive" } + + object Settings { + const val REQUESTER_PACKAGE_NAME = "REQUESTER_PACKAGE_NAME" + const val EXECUTOR_PACKAGE_NAME = "EXECUTOR_PACKAGE_NAME" + } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/compat/PackageInfoCompat.kt b/app/src/main/kotlin/dev/sanmer/pi/compat/PackageInfoCompat.kt new file mode 100644 index 00000000..fcd31fc7 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/compat/PackageInfoCompat.kt @@ -0,0 +1,14 @@ +package dev.sanmer.pi.compat + +import android.content.pm.PackageInfo +import android.content.pm.PackageInfoHidden +import dev.rikka.tools.refine.Refine + +object PackageInfoCompat { + val PackageInfo.isOverlayPackage get() = + Refine.unsafeCast(this) + .isOverlayPackage + + val PackageInfo.isPreinstalled get() = + lastUpdateTime <= 1230768000000 // 2009-01-01 08:00:00 GMT+8 +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/compat/PackageManagerCompat.kt b/app/src/main/kotlin/dev/sanmer/pi/compat/PackageManagerCompat.kt index 5a4556ad..5f57742e 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/compat/PackageManagerCompat.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/compat/PackageManagerCompat.kt @@ -1,9 +1,7 @@ package dev.sanmer.pi.compat -import android.content.ComponentName -import android.content.Intent -import android.content.IntentFilter import android.content.IntentSender +import android.content.pm.ApplicationInfo import android.content.pm.IPackageInstaller import android.content.pm.IPackageInstallerSession import android.content.pm.IPackageManager @@ -14,8 +12,8 @@ import android.content.pm.PackageInstallerHidden.SessionParamsHidden import android.content.pm.PackageManager import android.content.pm.PackageManagerHidden import android.content.pm.ParceledListSlice -import android.content.pm.ResolveInfo import android.content.pm.VersionedPackage +import android.os.Process import dev.rikka.tools.refine.Refine import dev.sanmer.pi.BuildConfig import kotlinx.coroutines.Dispatchers @@ -28,8 +26,6 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine object PackageManagerCompat { - private const val INSTALLER_PACKAGE_NAME= BuildConfig.APPLICATION_ID - private val packageManager: IPackageManager by lazy { IPackageManager.Stub.asInterface( ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) @@ -42,19 +38,23 @@ object PackageManagerCompat { ) } - private val packageInstaller: PackageInstaller by lazy { - if (BuildCompat.atLeastS) { + private fun getPackageInstaller(installerPackageName: String): PackageInstaller { + return if (BuildCompat.atLeastS) { Refine.unsafeCast( - PackageInstallerHidden(installer, INSTALLER_PACKAGE_NAME, null, 0) + PackageInstallerHidden(installer, installerPackageName, null, 0) ) } else { Refine.unsafeCast( - PackageInstallerHidden(installer, INSTALLER_PACKAGE_NAME, 0) + PackageInstallerHidden(installer, installerPackageName, 0) ) } } - private fun createSession(params: PackageInstaller.SessionParams): PackageInstaller.Session { + private fun createSession( + installerPackageName: String, + params: PackageInstaller.SessionParams + ): PackageInstaller.Session { + val packageInstaller = getPackageInstaller(installerPackageName) val sessionId = packageInstaller.createSession(params) val iSession = IPackageInstallerSession.Stub.asInterface( ShizukuBinderWrapper(installer.openSession(sessionId).asBinder()) @@ -62,13 +62,6 @@ object PackageManagerCompat { return Refine.unsafeCast(PackageInstallerHidden.SessionHidden(iSession)) } - private fun uninstallPackage(packageName: String, intentSender: IntentSender) { - installer.uninstall( - VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), - INSTALLER_PACKAGE_NAME, 0, intentSender, 0 - ) - } - fun getPackageUid(packageName: String, flags: Int, userId: Int): Int { return if (BuildCompat.atLeastT) { packageManager.getPackageUid(packageName, flags.toLong(), userId) @@ -85,6 +78,14 @@ object PackageManagerCompat { } } + fun getApplicationInfo(packageName: String, flags: Int, userId: Int): ApplicationInfo { + return if (BuildCompat.atLeastT) { + packageManager.getApplicationInfo(packageName, flags.toLong(), userId) + } else { + packageManager.getApplicationInfo(packageName, flags, userId) + } + } + fun getInstalledPackages(flags: Int, userId: Int): List { val packages: ParceledListSlice? = if (BuildCompat.atLeastT) { packageManager.getInstalledPackages(flags.toLong(), userId) @@ -99,56 +100,20 @@ object PackageManagerCompat { } } - fun addPreferredActivity(filter: IntentFilter, match: Int, set: Array, activity: ComponentName, userId: Int) { - if (BuildCompat.atLeastS) { - packageManager.addPreferredActivity(filter, match, set, activity, userId, true) - } else { - packageManager.addPreferredActivity(filter, match, set, activity, userId) - } - } - - fun clearPackagePreferredActivities(packageName: String) { - packageManager.clearPackagePreferredActivities(packageName) - } - - fun getPreferredActivities(packageName: String?): List> { - val outFilters = ArrayList() - val outActivities = ArrayList() - packageManager.getPreferredActivities(outFilters, outActivities, packageName) - - return outActivities.zip(outFilters) - } - - fun queryIntentActivities( - intent: Intent, - resolvedType: String, - flags: Int, - userId: Int - ): List { - val resolveInfo: ParceledListSlice? = if (BuildCompat.atLeastT) { - packageManager.queryIntentActivities(intent, resolvedType, flags.toLong(), userId) - } else { - packageManager.queryIntentActivities(intent, resolvedType, flags, userId) - } - - return if (resolveInfo != null) { - resolveInfo.list - } else { - emptyList() - } - } - - fun getHomeActivities(): ComponentName { - val outHomeCandidates = ArrayList() - return packageManager.getHomeActivities(outHomeCandidates) - } - - suspend fun install(packageFile: File, packageName: String, originatingPackageName: String?): Int = withContext(Dispatchers.IO) { + suspend fun install( + packageFile: File, + packageName: String, + installer: String, + originating: String + ): Int = withContext(Dispatchers.IO) { try { val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) - if (originatingPackageName != null) { - val originatingUid = getPackageUid(originatingPackageName, 0, 0) + val originatingUid = runCatching { + getPackageUid(originating, 0, 0) + }.getOrDefault(Process.INVALID_UID) + + if (originatingUid != Process.INVALID_UID) { params.setOriginatingUid(originatingUid) } @@ -157,7 +122,7 @@ object PackageManagerCompat { Refine.unsafeCast(params).installFlags = flags val input = packageFile.inputStream() - createSession(params).use { session -> + createSession(installer, params).use { session -> session.openWrite(packageName, 0, input.available().toLong()).use { output -> input.copyTo(output) session.fsync(output) @@ -182,22 +147,4 @@ object PackageManagerCompat { PackageInstaller.STATUS_FAILURE } } - - suspend fun uninstall(packageName: String): Int = withContext(Dispatchers.IO) { - try { - val intent = suspendCoroutine { cont -> - val adapter = IntentSenderCompat.IIntentSenderAdaptor { - cont.resume(it) - } - val intentSender = IntentSenderCompat.newInstance(adapter) - uninstallPackage(packageName, intentSender) - } - - Timber.i("Uninstall failed: $packageName") - intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) - } catch (e: Exception) { - Timber.e(e, "Uninstall failed: $packageName") - PackageInstaller.STATUS_FAILURE - } - } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/database/AppDatabase.kt b/app/src/main/kotlin/dev/sanmer/pi/database/AppDatabase.kt index 40efda6e..debe01ab 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/database/AppDatabase.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/database/AppDatabase.kt @@ -4,17 +4,31 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration import dev.sanmer.pi.database.dao.PackageDao +import dev.sanmer.pi.database.dao.SettingDao import dev.sanmer.pi.database.entity.PackageInfoEntity +import dev.sanmer.pi.database.entity.SettingEntity -@Database(entities = [PackageInfoEntity::class], version = 1) +@Database(entities = [PackageInfoEntity::class,SettingEntity::class], version = 2) abstract class AppDatabase : RoomDatabase() { abstract fun packageDao(): PackageDao + abstract fun settingDao(): SettingDao companion object { fun build(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, "pi") + .addMigrations( + MIGRATION_1_2 + ) .build() } + + private val MIGRATION_1_2 = Migration(1, 2) { + it.execSQL("CREATE TABLE IF NOT EXISTS settings (" + + "key TEXT NOT NULL, " + + "value TEXT NOT NULL, " + + "PRIMARY KEY(key))") + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/database/dao/SettingDao.kt b/app/src/main/kotlin/dev/sanmer/pi/database/dao/SettingDao.kt new file mode 100644 index 00000000..cb18aa58 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/database/dao/SettingDao.kt @@ -0,0 +1,23 @@ +package dev.sanmer.pi.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import dev.sanmer.pi.database.entity.SettingEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface SettingDao { + @Query("SELECT value FROM settings WHERE `key` = :key") + fun getByKeyAsFlow(key: String): Flow + + @Query("SELECT value FROM settings WHERE `key` = :key") + suspend fun getByKey(key: String): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(value: SettingEntity) + + @Query("DELETE FROM settings") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/database/di/DatabaseModule.kt b/app/src/main/kotlin/dev/sanmer/pi/database/di/DatabaseModule.kt index c9f5872f..64078e28 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/database/di/DatabaseModule.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/database/di/DatabaseModule.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dev.sanmer.pi.database.AppDatabase import dev.sanmer.pi.database.dao.PackageDao +import dev.sanmer.pi.database.dao.SettingDao import javax.inject.Singleton @Module @@ -22,4 +23,8 @@ object DatabaseModule { @Provides @Singleton fun providesPackageDao(db: AppDatabase): PackageDao = db.packageDao() + + @Provides + @Singleton + fun providesSettingDao(db: AppDatabase): SettingDao = db.settingDao() } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/database/entity/SettingEntity.kt b/app/src/main/kotlin/dev/sanmer/pi/database/entity/SettingEntity.kt new file mode 100644 index 00000000..8e002351 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/database/entity/SettingEntity.kt @@ -0,0 +1,10 @@ +package dev.sanmer.pi.database.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "settings") +data class SettingEntity( + @PrimaryKey val key: String, + val value: String +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/repository/UserPreferencesRepository.kt b/app/src/main/kotlin/dev/sanmer/pi/repository/UserPreferencesRepository.kt new file mode 100644 index 00000000..b04b636f --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/repository/UserPreferencesRepository.kt @@ -0,0 +1,35 @@ +package dev.sanmer.pi.repository + +import dev.sanmer.pi.BuildConfig +import dev.sanmer.pi.app.Const +import dev.sanmer.pi.database.dao.SettingDao +import dev.sanmer.pi.database.entity.SettingEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserPreferencesRepository @Inject constructor( + private val settingDao: SettingDao +) { + private suspend fun insert(key: String, value: String) = withContext(Dispatchers.IO) { + settingDao.insert(SettingEntity(key = key, value = value)) + } + + suspend fun getRequesterPackageNameOrDefault() = + settingDao.getByKey(Const.Settings.REQUESTER_PACKAGE_NAME) + ?: BuildConfig.APPLICATION_ID + + suspend fun setRequesterPackageName(value: String) = withContext(Dispatchers.IO) { + insert(Const.Settings.REQUESTER_PACKAGE_NAME, value) + } + + suspend fun getExecutorPackageNameOrDefault() = + settingDao.getByKey(Const.Settings.EXECUTOR_PACKAGE_NAME) + ?: BuildConfig.APPLICATION_ID + + suspend fun setExecutorPackageName(value: String) = withContext(Dispatchers.IO) { + insert(Const.Settings.EXECUTOR_PACKAGE_NAME, value) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt b/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt index 5ed01eb2..5ef703bc 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt @@ -14,9 +14,11 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint import dev.sanmer.pi.R import dev.sanmer.pi.app.utils.NotificationUtils import dev.sanmer.pi.compat.PackageManagerCompat +import dev.sanmer.pi.repository.UserPreferencesRepository import dev.sanmer.pi.utils.extensions.dp import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -28,11 +30,16 @@ import kotlinx.coroutines.launch import me.zhanghai.android.appiconloader.AppIconLoader import timber.log.Timber import java.io.File +import javax.inject.Inject +@AndroidEntryPoint class InstallService: LifecycleService() { private val context: Context by lazy { applicationContext } private val taskCount = MutableStateFlow(0) + @Inject + lateinit var userPreferencesRepository: UserPreferencesRepository + init { taskCount.drop(1) .onEach { @@ -52,7 +59,8 @@ class InstallService: LifecycleService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { lifecycleScope.launch { val packageFile = intent?.packageFile ?: return@launch - val originatingPackageName = intent.originatingPackageName + val originating = userPreferencesRepository.getRequesterPackageNameOrDefault() + val installer = userPreferencesRepository.getExecutorPackageNameOrDefault() taskCount.value += 1 val id = taskCount.value @@ -70,7 +78,8 @@ class InstallService: LifecycleService() { PackageManagerCompat.install( packageFile = packageFile, packageName = archiveInfo.packageName, - originatingPackageName = originatingPackageName + installer = installer, + originating = originating ) } @@ -165,20 +174,12 @@ class InstallService: LifecycleService() { private const val GROUP_KEY = "INSTALL_SERVICE_GROUP_KEY" private const val PARAM_PACKAGE_PATH = "PACKAGE_PATH" - private const val PARAM_ORIGINATING_PACKAGE_NAME = "ORIGINATING_PACKAGE_NAME" private val Intent.packagePathOrNull get() = getStringExtra(PARAM_PACKAGE_PATH) private val Intent.packageFile get() = checkNotNull(packagePathOrNull).let(::File) - private val Intent.originatingPackageName get() = getStringExtra( - PARAM_ORIGINATING_PACKAGE_NAME - ) - - fun Context.startInstallService( - packageFile: File, - originatingPackageName: String? - ) { + + fun Context.startInstallService(packageFile: File, ) { val intent = Intent(this, InstallService::class.java) intent.putExtra(PARAM_PACKAGE_PATH, packageFile.path) - intent.putExtra(PARAM_ORIGINATING_PACKAGE_NAME, originatingPackageName) startService(intent) } diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallActivity.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallActivity.kt index bc7fcbf8..95670a5d 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallActivity.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallActivity.kt @@ -3,7 +3,6 @@ package dev.sanmer.pi.ui.activity import android.app.Activity import android.content.Intent import android.content.pm.PackageInfo -import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -84,8 +83,7 @@ class InstallActivity : ComponentActivity() { private fun onOneTime() { startInstallService( - packageFile = tempFile, - originatingPackageName = sourceInfo?.packageName + packageFile = tempFile ) finish() @@ -141,11 +139,11 @@ class InstallActivity : ComponentActivity() { private fun getSourceInfo(callingPackage: String?): PackageInfo? { if (callingPackage == null) return null - return try { - return PackageManagerCompat.getPackageInfo(callingPackage, 0, 0) - } catch (ex: PackageManager.NameNotFoundException) { - null - } + return runCatching { + PackageManagerCompat.getPackageInfo( + callingPackage, 0, 0 + ) + }.getOrNull() } private fun getArchiveInfo(archiveFile: File): PackageInfo? { diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallScreen.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallScreen.kt index bf7dc5c9..1febc597 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallScreen.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/activity/InstallScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -26,6 +27,7 @@ import dev.sanmer.pi.ui.component.ConfirmationButtonsCenter import dev.sanmer.pi.ui.component.ConfirmationButtonsTop import dev.sanmer.pi.ui.component.ConfirmationDialog import dev.sanmer.pi.ui.component.LoadingDialog +import dev.sanmer.pi.ui.utils.formatStringResource @Composable fun InstallScreen( @@ -59,18 +61,20 @@ private fun ConfirmationDialog( onOneTime: () -> Unit, onDeny: () -> Unit ) = ConfirmationDialog( - onDismissRequest = {}, + onDismissRequest = onDeny, icon = { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { - AppIcon(sourceInfo) + if (sourceInfo != null) { + AppIcon(sourceInfo) - Icon( - painter = painterResource(id = R.drawable.arrow_right), - contentDescription = null - ) + Icon( + painter = painterResource(id = R.drawable.arrow_right), + contentDescription = null + ) + } AppIcon(archiveInfo) } @@ -79,8 +83,7 @@ private fun ConfirmationDialog( val pm = LocalContext.current.packageManager val sourceLabel by remember(sourceInfo) { derivedStateOf { - sourceInfo?.applicationInfo?.loadLabel(pm) - ?: "Unknown Source" + sourceInfo?.applicationInfo?.loadLabel(pm) ?: "" } } val archiveLabel by remember(archiveInfo) { @@ -90,21 +93,32 @@ private fun ConfirmationDialog( } Text( - text = stringResource(id = R.string.confirmation_message, - sourceLabel, archiveLabel), + text = formatStringResource( + style = { it.copy(fontStyle = FontStyle.Italic) }, + id = if (sourceInfo != null) { + R.string.confirmation_message + } else { + R.string.confirmation_message_unknown + }, sourceLabel, archiveLabel), textAlign = TextAlign.Center ) }, buttons = { ConfirmationButtonsTop( - text = stringResource(id = R.string.confirmation_allow_always), + text = stringResource(id = if (sourceInfo != null) { + R.string.confirmation_allow_always + } else { + R.string.confirmation_allow_always_unknown + }), onClick = onAlways ) - ConfirmationButtonsCenter( - text = stringResource(id = R.string.confirmation_allow_one_time), - onClick = onOneTime - ) + if (sourceInfo != null) { + ConfirmationButtonsCenter( + text = stringResource(id = R.string.confirmation_allow_one_time), + onClick = onOneTime + ) + } ConfirmationButtonsBottom( text = stringResource(id = R.string.confirmation_deny), diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/component/OverviewCard.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/component/OverviewCard.kt index 405d8c71..1235ffaf 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/component/OverviewCard.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/component/OverviewCard.kt @@ -1,6 +1,12 @@ package dev.sanmer.pi.ui.component import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,12 +26,14 @@ import androidx.compose.ui.unit.dp @Composable fun OverviewCard( - onClick: () -> Unit, @DrawableRes icon: Int, title: String, desc: String? = null, trailingIcon: (@Composable RowScope.() -> Unit)? = null, enable: Boolean = true, + onClick: () -> Unit = {}, + expanded: Boolean = false, + content: @Composable () -> Unit = {} ) = Surface( onClick = onClick, enabled = enable, @@ -32,39 +41,58 @@ fun OverviewCard( color = MaterialTheme.colorScheme.surface, tonalElevation = 1.dp, ) { - Row( + Column( modifier = Modifier - .padding(all = 16.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) + .animateContentSize(spring(stiffness = Spring.StiffnessMediumLow)), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top ) { - Logo( - modifier = Modifier.size(40.dp), - icon = icon, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp) + Row( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSecondaryContainer + Logo( + modifier = Modifier.size(40.dp), + icon = icon, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer, ) - if (desc != null) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { Text( - text = desc, - style = MaterialTheme.typography.bodyMedium, + text = title, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSecondaryContainer ) + + if (desc != null) { + Text( + text = desc, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } } + + trailingIcon?.invoke(this) } - trailingIcon?.invoke(this) + AnimatedVisibility( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + visible = expanded, + enter = fadeIn(spring(stiffness = Spring.StiffnessMedium)), + exit = fadeOut(spring(stiffness = Spring.StiffnessMedium)), + ) { + ProvideTextStyle( + value = MaterialTheme.typography.bodyLarge, + content = content + ) + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/AppList.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/AppList.kt new file mode 100644 index 00000000..dbc7c229 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/AppList.kt @@ -0,0 +1,63 @@ +package dev.sanmer.pi.ui.screens.home + +import android.content.pm.PackageInfo +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.sanmer.pi.R +import dev.sanmer.pi.ui.screens.home.items.AppItem +import dev.sanmer.pi.ui.utils.expandedShape + +@Composable +fun AppList( + onDismiss: () -> Unit, + packages: List, + onChoose: (PackageInfo) -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(), + shape = BottomSheetDefaults.expandedShape(15.dp), + windowInsets = WindowInsets.navigationBars + ) { + Text( + text = stringResource(id = R.string.home_select_app_title), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + LazyColumn( + modifier = Modifier.padding(top = 18.dp) + ) { + items( + items = packages, + key = { it.packageName } + ) { + Surface( + onClick = { + onChoose(it) + onDismiss() + }, + shape = RoundedCornerShape(15.dp) + ) { + AppItem(pi = it) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/HomeScreen.kt index bfc66620..104d709d 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/HomeScreen.kt @@ -1,20 +1,21 @@ package dev.sanmer.pi.ui.screens.home import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource @@ -23,11 +24,14 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import dev.sanmer.pi.R -import dev.sanmer.pi.ui.component.Logo +import dev.sanmer.pi.app.utils.ShizukuUtils import dev.sanmer.pi.ui.navigation.navigateToApps import dev.sanmer.pi.ui.screens.home.items.AuthorizedAppItem +import dev.sanmer.pi.ui.screens.home.items.ExecutorItem +import dev.sanmer.pi.ui.screens.home.items.RequesterItem import dev.sanmer.pi.ui.screens.home.items.ShizukuItem import dev.sanmer.pi.viewmodel.HomeViewModel +import java.lang.IllegalStateException @Composable fun HomeScreen( @@ -35,8 +39,12 @@ fun HomeScreen( viewModel: HomeViewModel = hiltViewModel() ) { val authorized by viewModel.authorized.collectAsStateWithLifecycle(0) - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + LaunchedEffect(key1 = ShizukuUtils.isEnable) { + viewModel.loadData() + } + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { @@ -58,6 +66,33 @@ fun HomeScreen( count = authorized, onClick = { navController.navigateToApps() } ) + + var isRequester by remember { mutableStateOf(false) } + RequesterItem( + pi = viewModel.requester, + onClick = { isRequester = true} + ) + + var isExecutor by remember { mutableStateOf(false) } + ExecutorItem( + pi = viewModel.executor, + onClick = { isExecutor = true} + ) + + if (isRequester || isExecutor) { + AppList( + onDismiss = { + if (isRequester) isRequester = false + if (isExecutor) isExecutor = false + }, + packages = viewModel.packages, + onChoose = when { + isRequester -> viewModel::setRequesterPackage + isExecutor -> viewModel::setExecutorPackage + else -> throw IllegalStateException() + } + ) + } } } } @@ -67,18 +102,5 @@ private fun TopBar( scrollBehavior: TopAppBarScrollBehavior ) = TopAppBar( title = { Text(text = stringResource(id = R.string.app_name)) }, - navigationIcon = { - Box( - modifier = Modifier.padding(horizontal = 18.dp) - ) { - Logo( - icon = R.drawable.launcher_outline, - modifier = Modifier.size(32.dp), - contentColor = MaterialTheme.colorScheme.onPrimary, - containerColor = MaterialTheme.colorScheme.primary, - fraction = 0.65f - ) - } - }, scrollBehavior = scrollBehavior ) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AppItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AppItem.kt new file mode 100644 index 00000000..d63a1f42 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AppItem.kt @@ -0,0 +1,66 @@ +package dev.sanmer.pi.ui.screens.home.items + +import android.content.pm.PackageInfo +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest + +@Composable +fun AppItem( + pi: PackageInfo, + modifier: Modifier = Modifier +) = Row( + modifier = modifier + .padding(all = 16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically +) { + val context = LocalContext.current + val label by remember(pi) { + derivedStateOf { + val pm = context.packageManager + pi.applicationInfo.loadLabel(pm) + } + } + + AsyncImage( + modifier = Modifier.size(45.dp), + model = ImageRequest.Builder(context) + .data(pi) + .crossfade(true) + .build(), + contentDescription = null + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = label.toString(), + style = MaterialTheme.typography.bodyLarge + ) + + Text( + text = pi.packageName, + style = MaterialTheme.typography.bodyMedium + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AuthorizedAppItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AuthorizedAppItem.kt index 238726ac..9e918002 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AuthorizedAppItem.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/AuthorizedAppItem.kt @@ -13,7 +13,7 @@ fun AuthorizedAppItem( onClick: () -> Unit ) = OverviewCard( icon = R.drawable.settings, - title = pluralStringResource(id = R.plurals.home_authorized_apps_count, count, count), + title = pluralStringResource(id = R.plurals.home_authorized_apps_count, count = count, count), desc = stringResource(id = R.string.home_view_authorized_apps), onClick = onClick, enable = ShizukuUtils.isEnable diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/ExecutorItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/ExecutorItem.kt new file mode 100644 index 00000000..e71a0ace --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/ExecutorItem.kt @@ -0,0 +1,40 @@ +package dev.sanmer.pi.ui.screens.home.items + +import android.content.pm.PackageInfo +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.dp +import dev.sanmer.pi.R +import dev.sanmer.pi.app.utils.ShizukuUtils +import dev.sanmer.pi.ui.component.OverviewCard +import dev.sanmer.pi.ui.utils.stringResource + +@Composable +fun ExecutorItem( + pi: PackageInfo?, + onClick: () -> Unit +) = OverviewCard( + icon = R.drawable.code, + title = stringResource(id = R.string.home_executor_title), + enable = false, + expanded = pi != null +) { + Surface( + onClick = onClick, + enabled = ShizukuUtils.isEnable, + shape = RoundedCornerShape(15.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + color = MaterialTheme.colorScheme.secondaryContainer + ) { + AppItem(pi = pi!!) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/RequesterItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/RequesterItem.kt new file mode 100644 index 00000000..fce0dc08 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/home/items/RequesterItem.kt @@ -0,0 +1,49 @@ +package dev.sanmer.pi.ui.screens.home.items + +import android.content.pm.PackageInfo +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import dev.sanmer.pi.R +import dev.sanmer.pi.app.utils.ShizukuUtils +import dev.sanmer.pi.ui.component.OverviewCard +import dev.sanmer.pi.ui.utils.stringResource + +@Composable +fun RequesterItem( + pi: PackageInfo?, + onClick: () -> Unit +) = OverviewCard( + icon = R.drawable.gift, + title = stringResource(id = R.string.home_requester_title), + enable = false, + expanded = pi != null +) { + Surface( + onClick = onClick, + enabled = ShizukuUtils.isEnable, + shape = RoundedCornerShape(15.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + color = MaterialTheme.colorScheme.secondaryContainer + ) { + AppItem(pi = pi!!) + } +} diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/utils/BottomSheetExt.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/utils/BottomSheetExt.kt new file mode 100644 index 00000000..516540b5 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/utils/BottomSheetExt.kt @@ -0,0 +1,9 @@ +package dev.sanmer.pi.ui.utils + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.ui.unit.Dp + +@Suppress("UnusedReceiverParameter") +fun BottomSheetDefaults.expandedShape(size: Dp) = + RoundedCornerShape(topStart = size, topEnd = size) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/utils/StringResExt.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/utils/StringResExt.kt index 7c3c573a..05124741 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/utils/StringResExt.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/utils/StringResExt.kt @@ -2,14 +2,22 @@ package dev.sanmer.pi.ui.utils import android.content.res.Resources import androidx.annotation.StringRes +import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle @Composable @ReadOnlyComposable -internal fun resources(): Resources { +private fun resources(): Resources { LocalConfiguration.current return LocalContext.current.resources } @@ -23,4 +31,46 @@ fun stringResource(@StringRes id: Int, @StringRes vararg formatArgs: Int): Strin }.toTypedArray() return resources.getString(id, *strings) +} + +@Composable +fun formatStringResource( + style: (SpanStyle) -> SpanStyle = { it }, + @StringRes id: Int, + vararg args: Any +): AnnotatedString { + val strRes = androidx.compose.ui.res.stringResource(id = id) + + val strList by remember { + derivedStateOf { strRes.split("%[1-${args.size}]\\\$s".toRegex()) } + } + + val argsMap by remember { + derivedStateOf { + buildMap(args.size) { + var argsLength = 0 + repeat(args.size) { + val char = "%${it + 1}\$s" + val index = strRes.indexOf(char) + argsLength + val value = args[it].toString().apply { + if (isNotEmpty()) argsLength += length - char.length + } + put(index, value) + } + } + } + } + + val currentStyle = LocalTextStyle.current.toSpanStyle() + val italicStyle by remember { derivedStateOf { style(currentStyle) } } + + return buildAnnotatedString { + strList.forEach { + if (argsMap.containsKey(length)) { + withStyle(italicStyle) { append(argsMap.getValue(length)) } + } + + withStyle(currentStyle) { append(it) } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/viewmodel/AppsViewModel.kt b/app/src/main/kotlin/dev/sanmer/pi/viewmodel/AppsViewModel.kt index a11ec10d..dde52cc1 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/viewmodel/AppsViewModel.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/viewmodel/AppsViewModel.kt @@ -1,13 +1,13 @@ package dev.sanmer.pi.viewmodel import android.Manifest +import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import androidx.compose.runtime.toMutableStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.sanmer.pi.BuildConfig import dev.sanmer.pi.compat.PackageManagerCompat import dev.sanmer.pi.model.IPackageInfo import dev.sanmer.pi.repository.LocalRepository @@ -66,25 +66,28 @@ class AppsViewModel @Inject constructor( } private suspend fun getPackages() = withContext(Dispatchers.IO) { - val allPackages: MutableList = ArrayList() - - runCatching { + val allPackages = runCatching { PackageManagerCompat.getInstalledPackages( - PackageManager.GET_PERMISSIONS, - 0 + PackageManager.GET_PERMISSIONS, 0 ) - }.onSuccess { - allPackages.addAll(it) }.onFailure { Timber.e(it, "getInstalledPackages") - } + }.getOrDefault(emptyList()) val isRequestedInstall: (PackageInfo) -> Boolean = { - it.requestedPermissions?.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES) == true + it.requestedPermissions?.contains( + Manifest.permission.REQUEST_INSTALL_PACKAGES + ) == true + } + + val isNotSystemApp: (PackageInfo) -> Boolean = { + it.applicationInfo.flags and (ApplicationInfo.FLAG_SYSTEM or + ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0 } allPackages.filter { - isRequestedInstall(it) + isRequestedInstall(it) && isNotSystemApp(it) && + it.applicationInfo.enabled } } diff --git a/app/src/main/kotlin/dev/sanmer/pi/viewmodel/HomeViewModel.kt b/app/src/main/kotlin/dev/sanmer/pi/viewmodel/HomeViewModel.kt index 4d9cb491..ae3433ec 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/viewmodel/HomeViewModel.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/viewmodel/HomeViewModel.kt @@ -1,19 +1,95 @@ package dev.sanmer.pi.viewmodel -import androidx.lifecycle.ViewModel +import android.app.Application +import android.content.Context +import android.content.pm.PackageInfo +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dev.sanmer.pi.app.utils.ShizukuUtils +import dev.sanmer.pi.compat.PackageInfoCompat.isOverlayPackage +import dev.sanmer.pi.compat.PackageInfoCompat.isPreinstalled +import dev.sanmer.pi.compat.PackageManagerCompat import dev.sanmer.pi.repository.LocalRepository +import dev.sanmer.pi.repository.UserPreferencesRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val localRepository: LocalRepository -) : ViewModel() { + private val localRepository: LocalRepository, + private val userPreferencesRepository: UserPreferencesRepository, + application: Application +) : AndroidViewModel(application) { + private val context: Context by lazy { getApplication() } + private val pm by lazy { context.packageManager } + val authorized get() = localRepository.getAuthorizedAllAsFlow().map { it.size } + var requester: PackageInfo? by mutableStateOf(null) + private set + var executor: PackageInfo? by mutableStateOf(null) + private set + var packages = listOf() + private set init { Timber.d("HomeViewModel init") } + + suspend fun loadData() { + viewModelScope.launch { + if (!ShizukuUtils.isEnable) return@launch + + val packagesDeferred = async { getPackages() } + + val requesterPackageName = userPreferencesRepository.getRequesterPackageNameOrDefault() + requester = PackageManagerCompat.getPackageInfo( + requesterPackageName, 0, 0 + ) + + val executorPackageName = userPreferencesRepository.getExecutorPackageNameOrDefault() + executor = PackageManagerCompat.getPackageInfo( + executorPackageName, 0, 0 + ) + + packages = packagesDeferred.await() + } + } + + private suspend fun getPackages() = withContext(Dispatchers.IO) { + val allPackages = runCatching { + PackageManagerCompat.getInstalledPackages(0, 0) + }.onFailure { + Timber.e(it, "getInstalledPackages") + }.getOrDefault(emptyList()) + + allPackages.filter { + !it.isOverlayPackage && !it.isPreinstalled + }.sortedBy { + it.applicationInfo.loadLabel(pm) + .toString().uppercase() + } + } + + fun setRequesterPackage(pi: PackageInfo) { + viewModelScope.launch { + requester = pi + userPreferencesRepository.setRequesterPackageName(pi.packageName) + } + } + + fun setExecutorPackage(pi: PackageInfo) { + viewModelScope.launch { + executor = pi + userPreferencesRepository.setExecutorPackageName(pi.packageName) + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/code.xml b/app/src/main/res/drawable/code.xml new file mode 100644 index 00000000..ad9104d9 --- /dev/null +++ b/app/src/main/res/drawable/code.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/gift.xml b/app/src/main/res/drawable/gift.xml new file mode 100644 index 00000000..d2d57d3c --- /dev/null +++ b/app/src/main/res/drawable/gift.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/ufo.xml b/app/src/main/res/drawable/ufo.xml deleted file mode 100644 index 44782503..00000000 --- a/app/src/main/res/drawable/ufo.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f8006725..12e2fd91 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,5 +1,6 @@ - + 安装 @@ -13,14 +14,19 @@ 已授权 %d 个应用 点按以管理授权的应用 + 安装请求方 + 安装执行方 + 选择应用 应用管理 空列表 - 允许%1$s安装%2$s吗? + 允许 %1$s 安装 %2$s 吗? + 允许安装 %2$s 吗? 始终允许 + 允许 仅限这一次 拒绝 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f825753f..9af65851 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,14 +14,19 @@ Authorized %d applications Tap to manage authorized apps + Installation Requester + Installation Executor + Select Application Application Management Empty list - Allow %1$s to install %2$s? + Allow %1$s to install %2$s? + Allow to install %2$s? Allow all the time + Allow Only this time Deny diff --git a/hidden-api/src/main/java/android/content/pm/PackageInfoHidden.java b/hidden-api/src/main/java/android/content/pm/PackageInfoHidden.java new file mode 100644 index 00000000..f59a1459 --- /dev/null +++ b/hidden-api/src/main/java/android/content/pm/PackageInfoHidden.java @@ -0,0 +1,13 @@ +package android.content.pm; + +import dev.rikka.tools.refine.RefineAs; + +@RefineAs(PackageInfo.class) +public class PackageInfoHidden { + + public String overlayTarget; + + public boolean isOverlayPackage() { + throw new RuntimeException("Stub!"); + } +} \ No newline at end of file