Merge "Add 'Archive' button to AppInfo screen" into main
This commit is contained in:
@@ -3896,6 +3896,8 @@
|
|||||||
<string name="controls_label">Controls</string>
|
<string name="controls_label">Controls</string>
|
||||||
<!-- Manage applications, text label for button to kill / force stop an application -->
|
<!-- Manage applications, text label for button to kill / force stop an application -->
|
||||||
<string name="force_stop">Force stop</string>
|
<string name="force_stop">Force stop</string>
|
||||||
|
<!-- Manage applications, text label for button to archive an application. Archiving means uninstalling the app without deleting user's personal data and replacing the app with a stub app with minimum size. So, the user can unarchive the app later and not lose any personal data. -->
|
||||||
|
<string name="archive">Archive</string>
|
||||||
<!-- Manage applications, individual application info screen,label under Storage heading. The total storage space taken up by this app. -->
|
<!-- Manage applications, individual application info screen,label under Storage heading. The total storage space taken up by this app. -->
|
||||||
<string name="total_size_label">Total</string>
|
<string name="total_size_label">Total</string>
|
||||||
<!-- Manage applications, individual application info screen, label under Storage heading. The amount of space taken up by the application itself (for example, the java compield files and things like that) -->
|
<!-- Manage applications, individual application info screen, label under Storage heading. The amount of space taken up by the application itself (for example, the java compield files and things like that) -->
|
||||||
@@ -4006,6 +4008,11 @@
|
|||||||
<!-- Manage applications, text for Move button -->
|
<!-- Manage applications, text for Move button -->
|
||||||
<string name="move_app">Move</string>
|
<string name="move_app">Move</string>
|
||||||
|
|
||||||
|
<!-- Toast message when archiving an app failed. -->
|
||||||
|
<string name="archiving_failed">Archiving failed</string>
|
||||||
|
<!-- Toast message when archiving an app succeeded. -->
|
||||||
|
<string name="archiving_succeeded">Archived <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
|
||||||
|
|
||||||
<!-- Text of pop up message if the request for a "migrate primary storage" operation
|
<!-- Text of pop up message if the request for a "migrate primary storage" operation
|
||||||
(see storage_menu_migrate) is denied as another is already in progress. [CHAR LIMIT=75] -->
|
(see storage_menu_migrate) is denied as another is already in progress. [CHAR LIMIT=75] -->
|
||||||
<string name="another_migration_already_in_progress">Another migration is already in progress.</string>
|
<string name="another_migration_already_in_progress">Another migration is already in progress.</string>
|
||||||
|
130
src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
Normal file
130
src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.settings.spa.app.appinfo
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CloudUpload
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settingslib.spa.widget.button.ActionButton
|
||||||
|
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
class AppArchiveButton(packageInfoPresenter: PackageInfoPresenter) {
|
||||||
|
private companion object {
|
||||||
|
private const val LOG_TAG = "AppArchiveButton"
|
||||||
|
private const val INTENT_ACTION = "com.android.settings.archive.action"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val context = packageInfoPresenter.context
|
||||||
|
private val appButtonRepository = AppButtonRepository(context)
|
||||||
|
private val userPackageManager = packageInfoPresenter.userPackageManager
|
||||||
|
private val packageInstaller = userPackageManager.packageInstaller
|
||||||
|
private val packageName = packageInfoPresenter.packageName
|
||||||
|
private val userHandle = UserHandle.of(packageInfoPresenter.userId)
|
||||||
|
private var broadcastReceiverIsCreated = false
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getActionButton(app: ApplicationInfo): ActionButton {
|
||||||
|
if (!broadcastReceiverIsCreated) {
|
||||||
|
val intentFilter = IntentFilter(INTENT_ACTION)
|
||||||
|
DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
|
||||||
|
if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) {
|
||||||
|
onReceive(intent, app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcastReceiverIsCreated = true
|
||||||
|
}
|
||||||
|
return ActionButton(
|
||||||
|
text = context.getString(R.string.archive),
|
||||||
|
imageVector = Icons.Outlined.CloudUpload,
|
||||||
|
enabled = remember(app) {
|
||||||
|
flow {
|
||||||
|
emit(
|
||||||
|
app.isActionButtonEnabled() && appButtonRepository.isAllowUninstallOrArchive(
|
||||||
|
context,
|
||||||
|
app
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
}.collectAsStateWithLifecycle(false).value
|
||||||
|
) { onArchiveClicked(app) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ApplicationInfo.isActionButtonEnabled(): Boolean {
|
||||||
|
return !isArchived
|
||||||
|
&& userPackageManager.isAppArchivable(packageName)
|
||||||
|
// We apply the same device policy for both the uninstallation and archive
|
||||||
|
// button.
|
||||||
|
&& !appButtonRepository.isUninstallBlockedByAdmin(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onArchiveClicked(app: ApplicationInfo) {
|
||||||
|
val intent = Intent(INTENT_ACTION)
|
||||||
|
intent.setPackage(context.packageName)
|
||||||
|
val pendingIntent = PendingIntent.getBroadcastAsUser(
|
||||||
|
context, 0, intent,
|
||||||
|
// FLAG_MUTABLE is required by PackageInstaller#requestArchive
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE,
|
||||||
|
userHandle
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender, 0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "Request archive failed", e)
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.archiving_failed),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReceive(intent: Intent, app: ApplicationInfo) {
|
||||||
|
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)) {
|
||||||
|
PackageInstaller.STATUS_SUCCESS -> {
|
||||||
|
val appLabel = userPackageManager.getApplicationLabel(app)
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.archiving_succeeded, appLabel),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Log.e(LOG_TAG, "Request archiving failed for $packageName with code $status")
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.archiving_failed),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -19,6 +19,7 @@ package com.android.settings.spa.app.appinfo
|
|||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.om.OverlayManager
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ResolveInfo
|
import android.content.pm.ResolveInfo
|
||||||
@@ -26,7 +27,9 @@ import com.android.settingslib.RestrictedLockUtils
|
|||||||
import com.android.settingslib.RestrictedLockUtilsInternal
|
import com.android.settingslib.RestrictedLockUtilsInternal
|
||||||
import com.android.settingslib.Utils
|
import com.android.settingslib.Utils
|
||||||
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
|
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
|
||||||
|
import com.android.settingslib.spaprivileged.model.app.hasFlag
|
||||||
import com.android.settingslib.spaprivileged.model.app.isDisallowControl
|
import com.android.settingslib.spaprivileged.model.app.isDisallowControl
|
||||||
|
import com.android.settingslib.spaprivileged.model.app.userHandle
|
||||||
import com.android.settingslib.spaprivileged.model.app.userId
|
import com.android.settingslib.spaprivileged.model.app.userId
|
||||||
|
|
||||||
class AppButtonRepository(private val context: Context) {
|
class AppButtonRepository(private val context: Context) {
|
||||||
@@ -77,6 +80,55 @@ class AppButtonRepository(private val context: Context) {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets whether a package can be uninstalled or archived. */
|
||||||
|
fun isAllowUninstallOrArchive(
|
||||||
|
context: Context, app: ApplicationInfo
|
||||||
|
): Boolean {
|
||||||
|
val overlayManager = checkNotNull(context.getSystemService(OverlayManager::class.java))
|
||||||
|
when {
|
||||||
|
!app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && !app.isArchived -> return false
|
||||||
|
|
||||||
|
com.android.settings.Utils.isProfileOrDeviceOwner(
|
||||||
|
context.devicePolicyManager, app.packageName, app.userId
|
||||||
|
) -> return false
|
||||||
|
|
||||||
|
isDisallowControl(app) -> return false
|
||||||
|
|
||||||
|
uninstallDisallowedDueToHomeApp(app.packageName) -> return false
|
||||||
|
|
||||||
|
// Resource overlays can be uninstalled iff they are public (installed on /data) and
|
||||||
|
// disabled. ("Enabled" means they are in use by resource management.)
|
||||||
|
app.isEnabledResourceOverlay(overlayManager) -> return false
|
||||||
|
|
||||||
|
else -> return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package cannot be uninstalled due to home app restrictions.
|
||||||
|
*
|
||||||
|
* Home launcher apps need special handling, we can't allow uninstallation of the only home
|
||||||
|
* app, and we don't want to allow uninstallation of an explicitly preferred one -- the user
|
||||||
|
* can go to Home settings and pick a different one, after which we'll permit uninstallation
|
||||||
|
* of the now-not-default one.
|
||||||
|
*/
|
||||||
|
private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean {
|
||||||
|
val homePackageInfo = getHomePackageInfo()
|
||||||
|
return when {
|
||||||
|
packageName !in homePackageInfo.homePackages -> false
|
||||||
|
|
||||||
|
// Disallow uninstall when this is the only home app.
|
||||||
|
homePackageInfo.homePackages.size == 1 -> true
|
||||||
|
|
||||||
|
// Disallow if this is the explicit default home app.
|
||||||
|
else -> packageName == homePackageInfo.currentDefaultHome?.packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ApplicationInfo.isEnabledResourceOverlay(overlayManager: OverlayManager): Boolean =
|
||||||
|
isResourceOverlay &&
|
||||||
|
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
|
||||||
|
|
||||||
data class HomePackages(
|
data class HomePackages(
|
||||||
val homePackages: Set<String>,
|
val homePackages: Set<String>,
|
||||||
val currentDefaultHome: ComponentName?,
|
val currentDefaultHome: ComponentName?,
|
||||||
|
@@ -30,7 +30,10 @@ import com.android.settingslib.spa.widget.button.ActionButtons
|
|||||||
/**
|
/**
|
||||||
* @param featureFlags can be overridden in tests
|
* @param featureFlags can be overridden in tests
|
||||||
*/
|
*/
|
||||||
fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) {
|
fun AppButtons(
|
||||||
|
packageInfoPresenter: PackageInfoPresenter,
|
||||||
|
featureFlags: FeatureFlags = FeatureFlagsImpl()
|
||||||
|
) {
|
||||||
if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
|
if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
|
||||||
val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
|
val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
|
||||||
ActionButtons(actionButtons = presenter.getActionButtons())
|
ActionButtons(actionButtons = presenter.getActionButtons())
|
||||||
@@ -49,6 +52,7 @@ private class AppButtonsPresenter(
|
|||||||
private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
|
private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
|
||||||
private val appClearButton = AppClearButton(packageInfoPresenter)
|
private val appClearButton = AppClearButton(packageInfoPresenter)
|
||||||
private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
|
private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
|
||||||
|
private val appArchiveButton = AppArchiveButton(packageInfoPresenter)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getActionButtons() =
|
fun getActionButtons() =
|
||||||
@@ -58,7 +62,11 @@ private class AppButtonsPresenter(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
|
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
|
||||||
if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
|
if (featureFlags.archiving()) {
|
||||||
|
appArchiveButton.getActionButton(app)
|
||||||
|
} else {
|
||||||
|
appLaunchButton.getActionButton(app)
|
||||||
|
},
|
||||||
appInstallButton.getActionButton(app),
|
appInstallButton.getActionButton(app),
|
||||||
appDisableButton.getActionButton(app),
|
appDisableButton.getActionButton(app),
|
||||||
appUninstallButton.getActionButton(app),
|
appUninstallButton.getActionButton(app),
|
||||||
|
@@ -18,7 +18,6 @@ package com.android.settings.spa.app.appinfo
|
|||||||
|
|
||||||
import android.app.settings.SettingsEnums
|
import android.app.settings.SettingsEnums
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.om.OverlayManager
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
@@ -28,11 +27,8 @@ 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 androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.android.settings.Utils
|
|
||||||
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
|
import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
|
||||||
import com.android.settingslib.spa.widget.button.ActionButton
|
import com.android.settingslib.spa.widget.button.ActionButton
|
||||||
import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
|
|
||||||
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.Dispatchers
|
||||||
@@ -42,7 +38,6 @@ 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
|
||||||
private val appButtonRepository = AppButtonRepository(context)
|
private val appButtonRepository = AppButtonRepository(context)
|
||||||
private val overlayManager = context.getSystemService(OverlayManager::class.java)!!
|
|
||||||
private val userManager = context.getSystemService(UserManager::class.java)!!
|
private val userManager = context.getSystemService(UserManager::class.java)!!
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -51,49 +46,6 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
|
|||||||
return uninstallButton(app)
|
return uninstallButton(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets whether a package can be uninstalled. */
|
|
||||||
private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when {
|
|
||||||
!app.hasFlag(ApplicationInfo.FLAG_INSTALLED) -> false
|
|
||||||
|
|
||||||
Utils.isProfileOrDeviceOwner(
|
|
||||||
context.devicePolicyManager, app.packageName, packageInfoPresenter.userId) -> false
|
|
||||||
|
|
||||||
appButtonRepository.isDisallowControl(app) -> false
|
|
||||||
|
|
||||||
uninstallDisallowedDueToHomeApp(app.packageName) -> false
|
|
||||||
|
|
||||||
// Resource overlays can be uninstalled iff they are public (installed on /data) and
|
|
||||||
// disabled. ("Enabled" means they are in use by resource management.)
|
|
||||||
app.isEnabledResourceOverlay() -> false
|
|
||||||
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given package cannot be uninstalled due to home app restrictions.
|
|
||||||
*
|
|
||||||
* Home launcher apps need special handling, we can't allow uninstallation of the only home
|
|
||||||
* app, and we don't want to allow uninstallation of an explicitly preferred one -- the user
|
|
||||||
* can go to Home settings and pick a different one, after which we'll permit uninstallation
|
|
||||||
* of the now-not-default one.
|
|
||||||
*/
|
|
||||||
private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean {
|
|
||||||
val homePackageInfo = appButtonRepository.getHomePackageInfo()
|
|
||||||
return when {
|
|
||||||
packageName !in homePackageInfo.homePackages -> false
|
|
||||||
|
|
||||||
// Disallow uninstall when this is the only home app.
|
|
||||||
homePackageInfo.homePackages.size == 1 -> true
|
|
||||||
|
|
||||||
// Disallow if this is the explicit default home app.
|
|
||||||
else -> packageName == homePackageInfo.currentDefaultHome?.packageName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ApplicationInfo.isEnabledResourceOverlay(): Boolean =
|
|
||||||
isResourceOverlay &&
|
|
||||||
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun uninstallButton(app: ApplicationInfo) = 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
|
||||||
@@ -101,7 +53,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
|
|||||||
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
|
imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
|
||||||
enabled = remember(app) {
|
enabled = remember(app) {
|
||||||
flow {
|
flow {
|
||||||
emit(isUninstallButtonEnabled(app))
|
emit(appButtonRepository.isAllowUninstallOrArchive(context, app))
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
}.collectAsStateWithLifecycle(false).value,
|
}.collectAsStateWithLifecycle(false).value,
|
||||||
) { onUninstallClicked(app) }
|
) { onUninstallClicked(app) }
|
||||||
|
@@ -87,19 +87,9 @@ class PackageInfoPresenter(
|
|||||||
).filter(::isInterestedAppChange).filter(::isForThisApp)
|
).filter(::isInterestedAppChange).filter(::isForThisApp)
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun isInterestedAppChange(intent: Intent) = when {
|
fun isInterestedAppChange(intent: Intent) =
|
||||||
intent.action != Intent.ACTION_PACKAGE_REMOVED -> true
|
intent.action != Intent.ACTION_PACKAGE_REMOVED ||
|
||||||
|
intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false)
|
||||||
// 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)
|
val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
|
||||||
.map { getPackageInfo() }
|
.map { getPackageInfo() }
|
||||||
|
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.settings.spa.app.appinfo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CloudUpload
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.android.settings.R
|
||||||
|
import com.android.settingslib.spa.widget.button.ActionButton
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.spy
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AppArchiveButtonTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
|
||||||
|
|
||||||
|
private val packageInfoPresenter = mock<PackageInfoPresenter>()
|
||||||
|
|
||||||
|
private val userPackageManager = mock<PackageManager>()
|
||||||
|
|
||||||
|
private val packageInstaller = mock<PackageInstaller>()
|
||||||
|
|
||||||
|
private lateinit var appArchiveButton: AppArchiveButton
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
whenever(packageInfoPresenter.context).thenReturn(context)
|
||||||
|
whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
|
||||||
|
whenever(userPackageManager.packageInstaller).thenReturn(packageInstaller)
|
||||||
|
whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
|
||||||
|
appArchiveButton = AppArchiveButton(packageInfoPresenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun appArchiveButton_whenIsArchived_isDisabled() {
|
||||||
|
val app = ApplicationInfo().apply {
|
||||||
|
packageName = PACKAGE_NAME
|
||||||
|
isArchived = true
|
||||||
|
}
|
||||||
|
whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
|
||||||
|
|
||||||
|
val actionButton = setContent(app)
|
||||||
|
|
||||||
|
assertThat(actionButton.enabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun appArchiveButton_whenIsNotAppArchivable_isDisabled() {
|
||||||
|
val app = ApplicationInfo().apply {
|
||||||
|
packageName = PACKAGE_NAME
|
||||||
|
isArchived = false
|
||||||
|
}
|
||||||
|
whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(false)
|
||||||
|
|
||||||
|
val actionButton = setContent(app)
|
||||||
|
|
||||||
|
assertThat(actionButton.enabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun appArchiveButton_displaysRightTextAndIcon() {
|
||||||
|
val app = ApplicationInfo().apply {
|
||||||
|
packageName = PACKAGE_NAME
|
||||||
|
isArchived = false
|
||||||
|
}
|
||||||
|
whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
|
||||||
|
|
||||||
|
val actionButton = setContent(app)
|
||||||
|
|
||||||
|
assertThat(actionButton.text).isEqualTo(context.getString(R.string.archive))
|
||||||
|
assertThat(actionButton.imageVector).isEqualTo(Icons.Outlined.CloudUpload)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun appArchiveButton_clicked() {
|
||||||
|
val app = ApplicationInfo().apply {
|
||||||
|
packageName = PACKAGE_NAME
|
||||||
|
isArchived = false
|
||||||
|
}
|
||||||
|
whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
|
||||||
|
|
||||||
|
val actionButton = setContent(app)
|
||||||
|
actionButton.onClick()
|
||||||
|
|
||||||
|
verify(packageInstaller).requestArchive(
|
||||||
|
eq(PACKAGE_NAME),
|
||||||
|
any(),
|
||||||
|
eq(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setContent(app: ApplicationInfo): ActionButton {
|
||||||
|
lateinit var actionButton: ActionButton
|
||||||
|
composeTestRule.setContent {
|
||||||
|
actionButton = appArchiveButton.getActionButton(app)
|
||||||
|
}
|
||||||
|
return actionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PACKAGE_NAME = "package.name"
|
||||||
|
}
|
||||||
|
}
|
@@ -22,8 +22,10 @@ import android.content.pm.ApplicationInfo
|
|||||||
import android.content.pm.FakeFeatureFlagsImpl
|
import android.content.pm.FakeFeatureFlagsImpl
|
||||||
import android.content.pm.Flags
|
import android.content.pm.Flags
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsEnabled
|
||||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
@@ -62,6 +64,9 @@ class AppButtonsTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var packageManager: PackageManager
|
private lateinit var packageManager: PackageManager
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var packageInstaller: PackageInstaller
|
||||||
|
|
||||||
private val featureFlags = FakeFeatureFlagsImpl()
|
private val featureFlags = FakeFeatureFlagsImpl()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -74,6 +79,7 @@ class AppButtonsTest {
|
|||||||
whenever(packageInfoPresenter.context).thenReturn(context)
|
whenever(packageInfoPresenter.context).thenReturn(context)
|
||||||
whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
|
whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
|
||||||
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
|
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
|
||||||
|
whenever(packageManager.packageInstaller).thenReturn(packageInstaller)
|
||||||
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
|
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
|
||||||
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
|
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
|
||||||
featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
|
featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
|
||||||
@@ -118,8 +124,24 @@ class AppButtonsTest {
|
|||||||
composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
|
composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setContent() {
|
@Test
|
||||||
whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO))
|
fun uninstallButton_enabled_whenAppIsArchived() {
|
||||||
|
whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
|
||||||
|
featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
|
||||||
|
val packageInfo = PackageInfo().apply {
|
||||||
|
applicationInfo = ApplicationInfo().apply {
|
||||||
|
packageName = PACKAGE_NAME
|
||||||
|
isArchived = true
|
||||||
|
}
|
||||||
|
packageName = PACKAGE_NAME
|
||||||
|
}
|
||||||
|
setContent(packageInfo)
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText(context.getString(R.string.uninstall_text)).assertIsEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setContent(packageInfo: PackageInfo = PACKAGE_INFO) {
|
||||||
|
whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(packageInfo))
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
AppButtons(packageInfoPresenter, featureFlags)
|
AppButtons(packageInfoPresenter, featureFlags)
|
||||||
}
|
}
|
||||||
|
@@ -105,6 +105,7 @@ class PackageInfoPresenterTest {
|
|||||||
fun isInterestedAppChange_archived_interested() {
|
fun isInterestedAppChange_archived_interested() {
|
||||||
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
|
val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
|
||||||
data = Uri.parse("package:$PACKAGE_NAME")
|
data = Uri.parse("package:$PACKAGE_NAME")
|
||||||
|
putExtra(Intent.EXTRA_ARCHIVAL, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
|
val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
|
||||||
|
Reference in New Issue
Block a user