Merge "Update the App Info Settings when package archived" into main
This commit is contained in:
@@ -119,16 +119,19 @@ object AppInfoSettingsProvider : SettingsPageProvider {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
|
private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) {
|
||||||
val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?:return
|
val packageInfoState = packageInfoPresenter.flow.collectAsStateWithLifecycle()
|
||||||
val app = checkNotNull(packageInfo.applicationInfo)
|
|
||||||
val featureFlags: FeatureFlags = FeatureFlagsImpl()
|
val featureFlags: FeatureFlags = FeatureFlagsImpl()
|
||||||
RegularScaffold(
|
RegularScaffold(
|
||||||
title = stringResource(R.string.application_info_label),
|
title = stringResource(R.string.application_info_label),
|
||||||
actions = {
|
actions = {
|
||||||
if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app)
|
packageInfoState.value?.applicationInfo?.let { app ->
|
||||||
AppInfoSettingsMoreOptions(packageInfoPresenter, app)
|
if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app)
|
||||||
|
AppInfoSettingsMoreOptions(packageInfoPresenter, app)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
val packageInfo = packageInfoState.value ?: return@RegularScaffold
|
||||||
|
val app = packageInfo.applicationInfo ?: return@RegularScaffold
|
||||||
val appInfoProvider = remember(packageInfo) { AppInfoProvider(packageInfo) }
|
val appInfoProvider = remember(packageInfo) { AppInfoProvider(packageInfo) }
|
||||||
|
|
||||||
appInfoProvider.AppInfo()
|
appInfoProvider.AppInfo()
|
||||||
|
@@ -23,8 +23,10 @@ import android.content.pm.ApplicationInfo
|
|||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.android.settings.Utils
|
import com.android.settings.Utils
|
||||||
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
|
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
|
||||||
@@ -33,6 +35,9 @@ import com.android.settingslib.spaprivileged.framework.common.devicePolicyManage
|
|||||||
import com.android.settingslib.spaprivileged.model.app.hasFlag
|
import com.android.settingslib.spaprivileged.model.app.hasFlag
|
||||||
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
|
import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
|
||||||
import com.android.settingslib.spaprivileged.model.app.userHandle
|
import com.android.settingslib.spaprivileged.model.app.userHandle
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
|
class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
|
||||||
private val context = packageInfoPresenter.context
|
private val context = packageInfoPresenter.context
|
||||||
@@ -43,7 +48,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
|
|||||||
@Composable
|
@Composable
|
||||||
fun getActionButton(app: ApplicationInfo): ActionButton? {
|
fun getActionButton(app: ApplicationInfo): ActionButton? {
|
||||||
if (app.isSystemApp || app.isInstantApp) return null
|
if (app.isSystemApp || app.isInstantApp) return null
|
||||||
return uninstallButton(app = app, enabled = isUninstallButtonEnabled(app))
|
return uninstallButton(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets whether a package can be uninstalled. */
|
/** Gets whether a package can be uninstalled. */
|
||||||
@@ -90,11 +95,15 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
|
|||||||
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
|
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun uninstallButton(app: ApplicationInfo, enabled: Boolean) = ActionButton(
|
private fun uninstallButton(app: ApplicationInfo) = ActionButton(
|
||||||
text = if (isCloneApp(app)) context.getString(R.string.delete) else
|
text = if (isCloneApp(app)) context.getString(R.string.delete) else
|
||||||
context.getString(R.string.uninstall_text),
|
context.getString(R.string.uninstall_text),
|
||||||
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
|
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
|
||||||
enabled = enabled,
|
enabled = remember(app) {
|
||||||
|
flow {
|
||||||
|
emit(isUninstallButtonEnabled(app))
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
}.collectAsStateWithLifecycle(false).value,
|
||||||
) { onUninstallClicked(app) }
|
) { onUninstallClicked(app) }
|
||||||
|
|
||||||
private fun onUninstallClicked(app: ApplicationInfo) {
|
private fun onUninstallClicked(app: ApplicationInfo) {
|
||||||
|
@@ -26,6 +26,7 @@ import android.content.pm.PackageInfo
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
|
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
|
||||||
import com.android.settings.spa.app.startUninstallActivity
|
import com.android.settings.spa.app.startUninstallActivity
|
||||||
@@ -40,6 +41,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
@@ -65,18 +67,42 @@ class PackageInfoPresenter(
|
|||||||
val userContext by lazy { context.asUser(userHandle) }
|
val userContext by lazy { context.asUser(userHandle) }
|
||||||
val userPackageManager: PackageManager by lazy { userContext.packageManager }
|
val userPackageManager: PackageManager by lazy { userContext.packageManager }
|
||||||
|
|
||||||
val flow: StateFlow<PackageInfo?> = merge(
|
private val appChangeFlow = context.broadcastReceiverAsUserFlow(
|
||||||
flowOf(null), // kick an initial value
|
intentFilter = IntentFilter().apply {
|
||||||
context.broadcastReceiverAsUserFlow(
|
// App enabled / disabled
|
||||||
intentFilter = IntentFilter().apply {
|
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
// App archived
|
||||||
addAction(Intent.ACTION_PACKAGE_RESTARTED)
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
addDataScheme("package")
|
|
||||||
},
|
// App updated / the updates are uninstalled (system app)
|
||||||
userHandle = userHandle,
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
),
|
|
||||||
).map { getPackageInfo() }
|
// App force-stopped
|
||||||
|
addAction(Intent.ACTION_PACKAGE_RESTARTED)
|
||||||
|
|
||||||
|
addDataScheme("package")
|
||||||
|
},
|
||||||
|
userHandle = userHandle,
|
||||||
|
).filter(::isInterestedAppChange).filter(::isForThisApp)
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun isInterestedAppChange(intent: Intent) = when {
|
||||||
|
intent.action != Intent.ACTION_PACKAGE_REMOVED -> true
|
||||||
|
|
||||||
|
// filter out the fully removed case, in which the page will be closed, so no need to
|
||||||
|
// refresh
|
||||||
|
intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) -> false
|
||||||
|
|
||||||
|
// filter out the updates are uninstalled (system app), which will followed by a replacing
|
||||||
|
// broadcast, we can refresh at that time
|
||||||
|
intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) -> false
|
||||||
|
|
||||||
|
else -> true // App archived
|
||||||
|
}
|
||||||
|
|
||||||
|
val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
|
||||||
|
.map { getPackageInfo() }
|
||||||
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,12 +115,14 @@ class PackageInfoPresenter(
|
|||||||
}
|
}
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
|
DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
|
||||||
if (packageName == intent.data?.schemeSpecificPart) {
|
if (isForThisApp(intent)) {
|
||||||
navController.navigateBack()
|
navController.navigateBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isForThisApp(intent: Intent) = packageName == intent.data?.schemeSpecificPart
|
||||||
|
|
||||||
/** Enables this package. */
|
/** Enables this package. */
|
||||||
fun enable() {
|
fun enable() {
|
||||||
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
|
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
|
||||||
|
@@ -20,7 +20,9 @@ import android.app.ActivityManager
|
|||||||
import android.app.settings.SettingsEnums
|
import android.app.settings.SettingsEnums
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.android.settings.testutils.FakeFeatureFactory
|
import com.android.settings.testutils.FakeFeatureFactory
|
||||||
@@ -61,11 +63,57 @@ class PackageInfoPresenterTest {
|
|||||||
private val fakeFeatureFactory = FakeFeatureFactory()
|
private val fakeFeatureFactory = FakeFeatureFactory()
|
||||||
private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
|
private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
|
||||||
|
|
||||||
|
private val packageInfoPresenter =
|
||||||
|
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isInterestedAppChange_packageChanged_isInterested() {
|
||||||
|
val intent = Intent(Intent.ACTION_PACKAGE_CHANGED).apply {
|
||||||
|
data = Uri.parse("package:$PACKAGE_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
|
||||||
|
|
||||||
|
assertThat(isInterestedAppChange).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isInterestedAppChange_fullyRemoved_notInterested() {
|
||||||
|
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
|
||||||
|
data = Uri.parse("package:$PACKAGE_NAME")
|
||||||
|
putExtra(Intent.EXTRA_DATA_REMOVED, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
|
||||||
|
|
||||||
|
assertThat(isInterestedAppChange).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isInterestedAppChange_removedBeforeReplacing_notInterested() {
|
||||||
|
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
|
||||||
|
data = Uri.parse("package:$PACKAGE_NAME")
|
||||||
|
putExtra(Intent.EXTRA_REPLACING, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
|
||||||
|
|
||||||
|
assertThat(isInterestedAppChange).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isInterestedAppChange_archived_interested() {
|
||||||
|
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
|
||||||
|
data = Uri.parse("package:$PACKAGE_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
|
||||||
|
|
||||||
|
assertThat(isInterestedAppChange).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun enable() = runBlocking {
|
fun enable() = runBlocking {
|
||||||
val packageInfoPresenter =
|
|
||||||
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
|
||||||
|
|
||||||
packageInfoPresenter.enable()
|
packageInfoPresenter.enable()
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
@@ -77,9 +125,6 @@ class PackageInfoPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun disable() = runBlocking {
|
fun disable() = runBlocking {
|
||||||
val packageInfoPresenter =
|
|
||||||
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
|
||||||
|
|
||||||
packageInfoPresenter.disable()
|
packageInfoPresenter.disable()
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
@@ -91,9 +136,6 @@ class PackageInfoPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun startUninstallActivity() = runBlocking {
|
fun startUninstallActivity() = runBlocking {
|
||||||
val packageInfoPresenter =
|
|
||||||
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
|
||||||
|
|
||||||
packageInfoPresenter.startUninstallActivity()
|
packageInfoPresenter.startUninstallActivity()
|
||||||
|
|
||||||
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
|
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
|
||||||
@@ -109,9 +151,6 @@ class PackageInfoPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun clearInstantApp() = runBlocking {
|
fun clearInstantApp() = runBlocking {
|
||||||
val packageInfoPresenter =
|
|
||||||
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
|
||||||
|
|
||||||
packageInfoPresenter.clearInstantApp()
|
packageInfoPresenter.clearInstantApp()
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
@@ -121,9 +160,6 @@ class PackageInfoPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun forceStop() = runBlocking {
|
fun forceStop() = runBlocking {
|
||||||
val packageInfoPresenter =
|
|
||||||
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
|
||||||
|
|
||||||
packageInfoPresenter.forceStop()
|
packageInfoPresenter.forceStop()
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
||||||
@@ -133,9 +169,6 @@ class PackageInfoPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun logAction() = runBlocking {
|
fun logAction() = runBlocking {
|
||||||
val packageInfoPresenter =
|
|
||||||
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
|
|
||||||
|
|
||||||
packageInfoPresenter.logAction(123)
|
packageInfoPresenter.logAction(123)
|
||||||
|
|
||||||
verifyAction(123)
|
verifyAction(123)
|
||||||
@@ -148,5 +181,6 @@ class PackageInfoPresenterTest {
|
|||||||
private companion object {
|
private companion object {
|
||||||
const val PACKAGE_NAME = "package.name"
|
const val PACKAGE_NAME = "package.name"
|
||||||
const val USER_ID = 0
|
const val USER_ID = 0
|
||||||
|
val PACKAGE_INFO = PackageInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user