diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e368d09c3dc..1f7e037f03a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4579,6 +4579,13 @@
+
+
+
-
-
-
generateBatteryEntryListFromBatteryUsageStats(
+ final Context context,
+ @Nullable final BatteryUsageStats batteryUsageStats,
+ @Nullable BatteryAppListPreferenceController batteryAppListPreferenceController) {
+ if (batteryUsageStats == null) {
+ Log.w(TAG, "batteryUsageStats is null content");
+ return null;
+ }
+ // Loads the battery consuming data.
+ final BatteryAppListPreferenceController controller =
+ batteryAppListPreferenceController == null
+ ? new BatteryAppListPreferenceController(
+ context,
+ /*preferenceKey=*/ null,
+ /*lifecycle=*/ null,
+ /*activity*=*/ null,
+ /*fragment=*/ null)
+ : batteryAppListPreferenceController;
+
+ return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true);
+ }
+
/**
* @return Returns the processed history map which has interpolated to every hour data.
* The start and end timestamp must be the even hours.
@@ -477,20 +527,14 @@ public final class DataProcessor {
final Context context) {
BatteryDiffData batteryDiffData = null;
try {
- final BatteryUsageStatsQuery batteryUsageStatsQuery =
- new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build();
- final BatteryUsageStats batteryUsageStats =
- context.getSystemService(BatteryStatsManager.class)
- .getBatteryUsageStats(batteryUsageStatsQuery);
-
- if (batteryUsageStats == null) {
- Log.w(TAG, "batteryUsageStats is null content");
- return null;
- }
-
+ final BatteryUsageStats batteryUsageStats = getBatteryUsageStats(context);
final List batteryEntryList =
- generateBatteryEntryListFromBatteryUsageStats(context, batteryUsageStats);
+ generateBatteryEntryListFromBatteryUsageStats(
+ context,
+ batteryUsageStats,
+ /*batteryAppListPreferenceController=*/ null);
batteryDiffData = generateBatteryDiffData(context, batteryEntryList, batteryUsageStats);
+ closeBatteryUsageStats(batteryUsageStats);
} catch (RuntimeException e) {
Log.e(TAG, "load batteryUsageStats:" + e);
}
@@ -498,20 +542,6 @@ public final class DataProcessor {
return batteryDiffData;
}
- @Nullable
- private static List generateBatteryEntryListFromBatteryUsageStats(
- final Context context, final BatteryUsageStats batteryUsageStats) {
- // Loads the battery consuming data.
- final BatteryAppListPreferenceController controller =
- new BatteryAppListPreferenceController(
- context,
- /*preferenceKey=*/ null,
- /*lifecycle=*/ null,
- /*activity*=*/ null,
- /*fragment=*/ null);
- return controller.getBatteryEntryList(batteryUsageStats, /*showAllApps=*/ true);
- }
-
@Nullable
private static List convertToBatteryHistEntry(
@Nullable final List batteryEntryList,
diff --git a/src/com/android/settings/spa/SpaDebugActivity.kt b/src/com/android/settings/spa/SpaDebugActivity.kt
index 40fe1d5822b..d9a4add1eec 100644
--- a/src/com/android/settings/spa/SpaDebugActivity.kt
+++ b/src/com/android/settings/spa/SpaDebugActivity.kt
@@ -18,5 +18,8 @@ package com.android.settings.spa
import com.android.settingslib.spa.framework.DebugActivity
-class SpaDebugActivity :
- DebugActivity(SpaEnvironment.settingsEntryRepository, SpaActivity::class.java)
+class SpaDebugActivity : DebugActivity(
+ SpaEnvironment.settingsEntryRepository,
+ browseActivityClass = SpaActivity::class.java,
+ entryProviderAuthorities = "com.android.settings.spa.provider",
+)
diff --git a/src/com/android/settings/spa/SpaEntryProvider.kt b/src/com/android/settings/spa/SpaEntryProvider.kt
new file mode 100644
index 00000000000..5fa7f564599
--- /dev/null
+++ b/src/com/android/settings/spa/SpaEntryProvider.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 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
+
+import com.android.settingslib.spa.framework.EntryProvider
+
+class SpaEntryProvider : EntryProvider(
+ SpaEnvironment.settingsEntryRepository,
+ browseActivityClass = SpaActivity::class.java,
+)
diff --git a/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt
new file mode 100644
index 00000000000..e1ee766603f
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/AppButtonRepository.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.UserManager
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.settingslib.Utils
+import com.android.settingslib.spaprivileged.model.app.userId
+
+class AppButtonRepository(private val context: Context) {
+ private val packageManager = context.packageManager
+ private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!!
+
+ /**
+ * Checks whether the given application is disallowed from modifying.
+ */
+ fun isDisallowControl(app: ApplicationInfo): Boolean = when {
+ // Not allow to control the device provisioning package.
+ Utils.isDeviceProvisioningPackage(context.resources, app.packageName) -> true
+
+ // If the uninstallation intent is already queued, disable the button.
+ devicePolicyManager.isUninstallInQueue(app.packageName) -> true
+
+ RestrictedLockUtilsInternal.hasBaseUserRestriction(
+ context, UserManager.DISALLOW_APPS_CONTROL, app.userId
+ ) -> true
+
+ else -> false
+ }
+
+ /**
+ * Checks whether the given application is an active admin.
+ */
+ fun isActiveAdmin(app: ApplicationInfo): Boolean =
+ devicePolicyManager.packageHasActiveAdmins(app.packageName, app.userId)
+
+ fun getHomePackageInfo(): AppUninstallButton.HomePackages {
+ val homePackages = mutableSetOf()
+ val homeActivities = ArrayList()
+ val currentDefaultHome = packageManager.getHomeActivities(homeActivities)
+ homeActivities.map { it.activityInfo }.forEach {
+ homePackages.add(it.packageName)
+ // Also make sure to include anything proxying for the home app
+ val metaPackageName = it.metaData?.getString(ActivityManager.META_HOME_ALTERNATE)
+ if (metaPackageName != null && signaturesMatch(metaPackageName, it.packageName)) {
+ homePackages.add(metaPackageName)
+ }
+ }
+ return AppUninstallButton.HomePackages(homePackages, currentDefaultHome)
+ }
+
+ private fun signaturesMatch(packageName1: String, packageName2: String): Boolean = try {
+ packageManager.checkSignatures(packageName1, packageName2) >= PackageManager.SIGNATURE_MATCH
+ } catch (e: Exception) {
+ // e.g. named alternate package not found during lookup; this is an expected case sometimes
+ false
+ }
+}
diff --git a/src/com/android/settings/spa/app/appsettings/AppButtons.kt b/src/com/android/settings/spa/app/appsettings/AppButtons.kt
new file mode 100644
index 00000000000..89f3b138df1
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/AppButtons.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.content.pm.PackageInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spa.widget.button.ActionButtons
+import kotlinx.coroutines.flow.map
+
+@Composable
+fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
+ val appButtonsHolder = remember { AppButtonsHolder(packageInfoPresenter) }
+ appButtonsHolder.Dialogs()
+ ActionButtons(actionButtons = appButtonsHolder.rememberActionsButtons().value)
+}
+
+private class AppButtonsHolder(private val packageInfoPresenter: PackageInfoPresenter) {
+ private val appLaunchButton = AppLaunchButton(context = packageInfoPresenter.context)
+ private val appDisableButton = AppDisableButton(packageInfoPresenter)
+ private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
+ private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
+
+ @Composable
+ fun rememberActionsButtons() = remember {
+ packageInfoPresenter.flow.map { packageInfo ->
+ if (packageInfo != null) getActionButtons(packageInfo) else emptyList()
+ }
+ }.collectAsState(initial = emptyList())
+
+ private fun getActionButtons(packageInfo: PackageInfo): List = listOfNotNull(
+ appLaunchButton.getActionButton(packageInfo),
+ appDisableButton.getActionButton(packageInfo),
+ appUninstallButton.getActionButton(packageInfo),
+ appForceStopButton.getActionButton(packageInfo),
+ )
+
+ @Composable
+ fun Dialogs() {
+ appDisableButton.DisableConfirmDialog()
+ appForceStopButton.ForceStopConfirmDialog()
+ }
+}
diff --git a/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt
new file mode 100644
index 00000000000..6f5e671f8a8
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/AppDisableButton.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.app.admin.DevicePolicyManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.os.UserManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowCircleDown
+import androidx.compose.material.icons.outlined.HideSource
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import com.android.settings.R
+import com.android.settings.Utils
+import com.android.settings.overlay.FeatureFactory
+import com.android.settingslib.Utils as SettingsLibUtils
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.model.app.hasFlag
+import com.android.settingslib.spaprivileged.model.app.isDisabledUntilUsed
+
+class AppDisableButton(
+ private val packageInfoPresenter: PackageInfoPresenter,
+) {
+ private val context = packageInfoPresenter.context
+ private val appButtonRepository = AppButtonRepository(context)
+ private val resources = context.resources
+ private val packageManager = context.packageManager
+ private val userManager = context.getSystemService(UserManager::class.java)!!
+ private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!!
+ private val applicationFeatureProvider =
+ FeatureFactory.getFactory(context).getApplicationFeatureProvider(context)
+
+ private var openConfirmDialog by mutableStateOf(false)
+
+ fun getActionButton(packageInfo: PackageInfo): ActionButton? {
+ val app = packageInfo.applicationInfo
+ if (!app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) return null
+
+ return when {
+ app.enabled && !app.isDisabledUntilUsed() -> {
+ disableButton(enabled = isDisableButtonEnabled(packageInfo))
+ }
+
+ else -> enableButton()
+ }
+ }
+
+ /**
+ * Gets whether a package can be disabled.
+ */
+ private fun isDisableButtonEnabled(packageInfo: PackageInfo): Boolean {
+ val packageName = packageInfo.packageName
+ val app = packageInfo.applicationInfo
+ return when {
+ packageName in applicationFeatureProvider.keepEnabledPackages -> false
+
+ // Home launcher apps need special handling. In system ones we don't risk downgrading
+ // because that can interfere with home-key resolution.
+ packageName in appButtonRepository.getHomePackageInfo().homePackages -> false
+
+ // Try to prevent the user from bricking their phone by not allowing disabling of apps
+ // signed with the system certificate.
+ SettingsLibUtils.isSystemPackage(resources, packageManager, packageInfo) -> false
+
+ // If this is a device admin, it can't be disabled.
+ appButtonRepository.isActiveAdmin(app) -> false
+
+ // We don't allow disabling DO/PO on *any* users if it's a system app, because
+ // "disabling" is actually "downgrade to the system version + disable", and "downgrade"
+ // will clear data on all users.
+ Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, packageName) -> false
+
+ appButtonRepository.isDisallowControl(app) -> false
+
+ // system/vendor resource overlays can never be disabled.
+ app.isResourceOverlay -> false
+
+ else -> true
+ }
+ }
+
+ private fun disableButton(enabled: Boolean) = ActionButton(
+ text = context.getString(R.string.disable_text),
+ imageVector = Icons.Outlined.HideSource,
+ enabled = enabled,
+ ) { openConfirmDialog = true }
+
+ private fun enableButton() = ActionButton(
+ text = context.getString(R.string.enable_text),
+ imageVector = Icons.Outlined.ArrowCircleDown,
+ ) { packageInfoPresenter.enable() }
+
+ @Composable
+ fun DisableConfirmDialog() {
+ if (!openConfirmDialog) return
+ AlertDialog(
+ onDismissRequest = { openConfirmDialog = false },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ openConfirmDialog = false
+ packageInfoPresenter.disable()
+ },
+ ) {
+ Text(stringResource(R.string.app_disable_dlg_positive))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { openConfirmDialog = false }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ text = {
+ Text(stringResource(R.string.app_disable_dlg_text))
+ },
+ )
+ }
+}
diff --git a/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt
new file mode 100644
index 00000000000..49191637677
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/AppForceStopButton.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.app.settings.SettingsEnums
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.os.UserManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.WarningAmber
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import com.android.settings.R
+import com.android.settingslib.RestrictedLockUtils
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.model.app.hasFlag
+import com.android.settingslib.spaprivileged.model.app.userId
+
+class AppForceStopButton(
+ private val packageInfoPresenter: PackageInfoPresenter,
+) {
+ private val context = packageInfoPresenter.context
+ private val appButtonRepository = AppButtonRepository(context)
+ private val packageManager = context.packageManager
+
+ private var openConfirmDialog by mutableStateOf(false)
+
+ fun getActionButton(packageInfo: PackageInfo): ActionButton {
+ val app = packageInfo.applicationInfo
+ return ActionButton(
+ text = context.getString(R.string.force_stop),
+ imageVector = Icons.Outlined.WarningAmber,
+ enabled = isForceStopButtonEnable(app),
+ ) { onForceStopButtonClicked(app) }
+ }
+
+ /**
+ * Gets whether a package can be force stopped.
+ */
+ private fun isForceStopButtonEnable(app: ApplicationInfo): Boolean = when {
+ // User can't force stop device admin.
+ appButtonRepository.isActiveAdmin(app) -> false
+
+ appButtonRepository.isDisallowControl(app) -> false
+
+ // If the app isn't explicitly stopped, then always show the force stop button.
+ else -> !app.hasFlag(ApplicationInfo.FLAG_STOPPED)
+ }
+
+ private fun onForceStopButtonClicked(app: ApplicationInfo) {
+ packageInfoPresenter.logAction(SettingsEnums.ACTION_APP_INFO_FORCE_STOP)
+ getAdminRestriction(app)?.let { admin ->
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin)
+ return
+ }
+ openConfirmDialog = true
+ }
+
+ private fun getAdminRestriction(app: ApplicationInfo): EnforcedAdmin? = when {
+ packageManager.isPackageStateProtected(app.packageName, app.userId) -> {
+ RestrictedLockUtilsInternal.getDeviceOwner(context)
+ }
+
+ else -> RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+ context, UserManager.DISALLOW_APPS_CONTROL, app.userId
+ )
+ }
+
+ @Composable
+ fun ForceStopConfirmDialog() {
+ if (!openConfirmDialog) return
+ AlertDialog(
+ onDismissRequest = { openConfirmDialog = false },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ openConfirmDialog = false
+ packageInfoPresenter.forceStop()
+ },
+ ) {
+ Text(stringResource(R.string.okay))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { openConfirmDialog = false }) {
+ Text(stringResource(R.string.cancel))
+ }
+ },
+ title = {
+ Text(stringResource(R.string.force_stop_dlg_title))
+ },
+ text = {
+ Text(stringResource(R.string.force_stop_dlg_text))
+ },
+ )
+ }
+}
diff --git a/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt b/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt
new file mode 100644
index 00000000000..a983b675938
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/AppLaunchButton.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Launch
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.model.app.userHandle
+
+class AppLaunchButton(private val context: Context) {
+ private val packageManager = context.packageManager
+
+ fun getActionButton(packageInfo: PackageInfo): ActionButton? =
+ packageManager.getLaunchIntentForPackage(packageInfo.packageName)?.let { intent ->
+ launchButton(intent, packageInfo.applicationInfo)
+ }
+
+ private fun launchButton(intent: Intent, app: ApplicationInfo) = ActionButton(
+ text = context.getString(R.string.launch_instant_app),
+ imageVector = Icons.Outlined.Launch,
+ ) {
+ context.startActivityAsUser(intent, app.userHandle)
+ }
+}
diff --git a/src/com/android/settings/spa/app/appsettings/AppSettings.kt b/src/com/android/settings/spa/app/appsettings/AppSettings.kt
index d0a230f1ddd..4c960e5a17a 100644
--- a/src/com/android/settings/spa/app/appsettings/AppSettings.kt
+++ b/src/com/android/settings/spa/app/appsettings/AppSettings.kt
@@ -17,10 +17,12 @@
package com.android.settings.spa.app.appsettings
import android.content.pm.ApplicationInfo
-import android.content.pm.PackageInfo
import android.os.Bundle
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavType
import androidx.navigation.navArgument
@@ -34,7 +36,6 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category
-import com.android.settingslib.spaprivileged.model.app.PackageManagers
import com.android.settingslib.spaprivileged.model.app.toRoute
import com.android.settingslib.spaprivileged.template.app.AppInfoProvider
@@ -53,22 +54,36 @@ object AppSettingsProvider : SettingsPageProvider {
override fun Page(arguments: Bundle?) {
val packageName = arguments!!.getString(PACKAGE_NAME)!!
val userId = arguments.getInt(USER_ID)
- remember { PackageManagers.getPackageInfoAsUser(packageName, userId) }?.let {
- AppSettings(it)
+ val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val packageInfoPresenter = remember {
+ PackageInfoPresenter(context, packageName, userId, coroutineScope)
}
+ AppSettings(packageInfoPresenter)
+ packageInfoPresenter.PageCloser()
}
@Composable
fun navigator(app: ApplicationInfo) = navigator(route = "$name/${app.toRoute()}")
+
+ /**
+ * Gets the route to the App Settings page.
+ *
+ * Expose route to enable enter from non-SPA pages.
+ */
+ fun getRoute(packageName: String, userId: Int): String = "$name/$packageName/$userId"
}
@Composable
-private fun AppSettings(packageInfo: PackageInfo) {
+private fun AppSettings(packageInfoPresenter: PackageInfoPresenter) {
+ val packageInfo = packageInfoPresenter.flow.collectAsState().value ?: return
RegularScaffold(title = stringResource(R.string.application_info_label)) {
val appInfoProvider = remember { AppInfoProvider(packageInfo) }
appInfoProvider.AppInfo()
+ AppButtons(packageInfoPresenter)
+
Category(title = stringResource(R.string.advanced_apps)) {
val app = packageInfo.applicationInfo
DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)
diff --git a/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt
new file mode 100644
index 00000000000..53b36b99d11
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/AppUninstallButton.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.app.admin.DevicePolicyManager
+import android.app.settings.SettingsEnums
+import android.content.ComponentName
+import android.content.Intent
+import android.content.om.OverlayManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.net.Uri
+import android.os.UserManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import com.android.settings.R
+import com.android.settings.Utils
+import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
+import com.android.settingslib.RestrictedLockUtils
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.model.app.hasFlag
+import com.android.settingslib.spaprivileged.model.app.userHandle
+import com.android.settingslib.spaprivileged.model.app.userId
+
+class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
+ private val context = packageInfoPresenter.context
+ private val appButtonRepository = AppButtonRepository(context)
+ private val userManager = context.getSystemService(UserManager::class.java)!!
+ private val overlayManager = context.getSystemService(OverlayManager::class.java)!!
+ private val devicePolicyManager = context.getSystemService(DevicePolicyManager::class.java)!!
+
+ fun getActionButton(packageInfo: PackageInfo): ActionButton? {
+ val app = packageInfo.applicationInfo
+ if (app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) return null
+ return uninstallButton(app = app, enabled = isUninstallButtonEnabled(app))
+ }
+
+ /** Gets whether a package can be uninstalled. */
+ private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when {
+ // When we have multiple users, there is a separate menu to uninstall for all users.
+ !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && userManager.users.size >= 2 -> false
+
+ // Not allow to uninstall DO/PO.
+ Utils.isProfileOrDeviceOwner(devicePolicyManager, app.packageName, app.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
+
+ private fun uninstallButton(app: ApplicationInfo, enabled: Boolean) = ActionButton(
+ text = context.getString(R.string.uninstall_text),
+ imageVector = Icons.Outlined.Delete,
+ enabled = enabled,
+ ) { onUninstallClicked(app) }
+
+ private fun onUninstallClicked(app: ApplicationInfo) {
+ if (appButtonRepository.isActiveAdmin(app)) {
+ packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN)
+ val intent = Intent(context, DeviceAdminAdd::class.java).apply {
+ putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, app.packageName)
+ }
+ context.startActivityAsUser(intent, app.userHandle)
+ return
+ }
+ RestrictedLockUtilsInternal.checkIfUninstallBlocked(
+ context, app.packageName, app.userId
+ )?.let { admin ->
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin)
+ return
+ }
+ startUninstallActivity(app)
+ }
+
+ data class HomePackages(
+ val homePackages: Set,
+ val currentDefaultHome: ComponentName?,
+ )
+
+ private fun startUninstallActivity(app: ApplicationInfo) {
+ val packageUri = Uri.parse("package:${app.packageName}")
+ packageInfoPresenter.logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
+ val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri).apply {
+ putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, !app.hasFlag(ApplicationInfo.FLAG_INSTALLED))
+ }
+ context.startActivityAsUser(intent, app.userHandle)
+ }
+}
diff --git a/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt
new file mode 100644
index 00000000000..a00f83c9002
--- /dev/null
+++ b/src/com/android/settings/spa/app/appsettings/PackageInfoPresenter.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 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.appsettings
+
+import android.app.ActivityManager
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.util.Log
+import androidx.compose.runtime.Composable
+import com.android.settings.overlay.FeatureFactory
+import com.android.settingslib.spa.framework.compose.DisposableBroadcastReceiverAsUser
+import com.android.settingslib.spa.framework.compose.LocalNavController
+import com.android.settingslib.spaprivileged.model.app.PackageManagers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+private const val TAG = "PackageInfoPresenter"
+
+/**
+ * Presenter which helps to present the status change of [PackageInfo].
+ */
+class PackageInfoPresenter(
+ val context: Context,
+ val packageName: String,
+ val userId: Int,
+ private val coroutineScope: CoroutineScope,
+) {
+ private val metricsFeatureProvider = FeatureFactory.getFactory(context).metricsFeatureProvider
+ private val packageManager by lazy {
+ context.createContextAsUser(UserHandle.of(userId), 0).packageManager
+ }
+ private val _flow: MutableStateFlow = MutableStateFlow(null)
+
+ val flow: StateFlow = _flow
+
+ init {
+ notifyChange()
+ }
+
+ private fun notifyChange() {
+ coroutineScope.launch(Dispatchers.IO) {
+ _flow.value = getPackageInfo()
+ }
+ }
+
+ /**
+ * Closes the page when the package is uninstalled.
+ */
+ @Composable
+ fun PageCloser() {
+ val intentFilter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED).apply {
+ addDataScheme("package")
+ }
+ val navController = LocalNavController.current
+ DisposableBroadcastReceiverAsUser(userId, intentFilter) { intent ->
+ if (packageName == intent.data?.schemeSpecificPart) {
+ navController.navigateBack()
+ }
+ }
+ }
+
+ /** Enables this package. */
+ fun enable() {
+ logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
+ coroutineScope.launch(Dispatchers.IO) {
+ packageManager.setApplicationEnabledSetting(
+ packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0
+ )
+ notifyChange()
+ }
+ }
+
+ /** Disables this package. */
+ fun disable() {
+ logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
+ coroutineScope.launch(Dispatchers.IO) {
+ packageManager.setApplicationEnabledSetting(
+ packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
+ )
+ notifyChange()
+ }
+ }
+
+ /** Force stops this package. */
+ fun forceStop() {
+ logAction(SettingsEnums.ACTION_APP_FORCE_STOP)
+ coroutineScope.launch(Dispatchers.Default) {
+ val activityManager = context.getSystemService(ActivityManager::class.java)!!
+ Log.d(TAG, "Stopping package $packageName")
+ activityManager.forceStopPackageAsUser(packageName, userId)
+ notifyChange()
+ }
+ }
+
+ fun logAction(category: Int) {
+ metricsFeatureProvider.action(context, category, packageName)
+ }
+
+ private fun getPackageInfo() =
+ PackageManagers.getPackageInfoAsUser(
+ packageName = packageName,
+ flags = PackageManager.MATCH_DISABLED_COMPONENTS or
+ PackageManager.GET_SIGNATURES or
+ PackageManager.GET_PERMISSIONS,
+ userId = userId,
+ )
+}
diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java
index 2571426be1e..942d5caf91d 100644
--- a/src/com/android/settings/users/UserSettings.java
+++ b/src/com/android/settings/users/UserSettings.java
@@ -1196,11 +1196,19 @@ public class UserSettings extends SettingsPreferenceFragment
if (context == null) {
return;
}
- final List users = mUserManager.getAliveUsers()
- // Only users that can be switched to should show up here.
- // e.g. Managed profiles appear under Accounts Settings instead
- .stream().filter(UserInfo::supportsSwitchToByUser)
- .collect(Collectors.toList());
+
+ List users;
+ if (mUserCaps.mUserSwitcherEnabled) {
+ // Only users that can be switched to should show up here.
+ // e.g. Managed profiles appear under Accounts Settings instead
+ users = mUserManager.getAliveUsers().stream()
+ .filter(UserInfo::supportsSwitchToByUser)
+ .collect(Collectors.toList());
+ } else {
+ // Only current user will be displayed in case of multi-user switch is disabled
+ users = List.of(mUserManager.getUserInfo(context.getUserId()));
+ }
+
final ArrayList missingIcons = new ArrayList<>();
final ArrayList userPreferences = new ArrayList<>();
@@ -1276,7 +1284,6 @@ public class UserSettings extends SettingsPreferenceFragment
userPreferences.add(pref);
}
-
// Sort list of users by serialNum
Collections.sort(userPreferences, UserPreference.SERIAL_NUMBER_COMPARATOR);
@@ -1299,7 +1306,6 @@ public class UserSettings extends SettingsPreferenceFragment
// Remove everything from mUserListCategory and add new users.
mUserListCategory.removeAll();
- // If multi-user is disabled, just show top info and return.
final Preference addUserOnLockScreen = getPreferenceScreen().findPreference(
mAddUserWhenLockedPreferenceController.getPreferenceKey());
mAddUserWhenLockedPreferenceController.updateState(addUserOnLockScreen);
@@ -1311,16 +1317,11 @@ public class UserSettings extends SettingsPreferenceFragment
final Preference multiUserTopIntroPreference = getPreferenceScreen().findPreference(
mMultiUserTopIntroPreferenceController.getPreferenceKey());
mMultiUserTopIntroPreferenceController.updateState(multiUserTopIntroPreference);
- mUserListCategory.setVisible(mUserCaps.mUserSwitcherEnabled);
updateGuestPreferences();
updateGuestCategory(context, users);
updateAddUser(context);
updateAddSupervisedUser(context);
- if (!mUserCaps.mUserSwitcherEnabled) {
- return;
- }
-
for (UserPreference userPreference : userPreferences) {
userPreference.setOrder(Preference.DEFAULT_ORDER);
mUserListCategory.addPreference(userPreference);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
index c4832041314..df9d8655109 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
@@ -579,9 +579,13 @@ public class DataProcessorTest {
ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 25.0,
/*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 3);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
+ 3);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
+ 0);
}
@Test
@@ -674,9 +678,13 @@ public class DataProcessorTest {
assertThat(resultMap.get(0).get(0)).isNotNull();
assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull();
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 2);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
+ 2);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
+ 0);
}
@Test
@@ -739,9 +747,13 @@ public class DataProcessorTest {
assertThat(resultMap.get(0).get(0)).isNotNull();
assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull();
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 1);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
+ 1);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
+ 0);
}
@Test
@@ -814,9 +826,13 @@ public class DataProcessorTest {
ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0,
/*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 20);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 1);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
+ 1);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 1);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
+ 1);
}
@Test
@@ -889,9 +905,13 @@ public class DataProcessorTest {
resultEntry = resultDiffData.getAppDiffEntryList().get(1);
assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(0);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT, 2);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_SHOWN_APP_COUNT,
+ 2);
verify(mMetricsFeatureProvider)
- .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT, 0);
+ .action(mContext.getApplicationContext(),
+ SettingsEnums.ACTION_BATTERY_USAGE_HIDDEN_APP_COUNT,
+ 0);
}
@Test
diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java
index 1f8c0b40809..62a8a75a621 100644
--- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java
@@ -177,6 +177,7 @@ public class UserSettingsTest {
doReturn(prefs).when(mMockPreferenceManager).getSharedPreferences();
doReturn(mContext).when(mMockPreferenceManager).getContext();
doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen();
+ doReturn(ACTIVE_USER_ID).when(mContext).getUserId();
mFragment.mMePreference = mMePreference;
mFragment.mAddUser = mAddUserPreference;
@@ -443,6 +444,7 @@ public class UserSettingsTest {
@Test
public void updateUserList_userSwitcherDisabled_shouldNotShowAddUser() {
+ givenUsers(getAdminUser(true));
mUserCapabilities.mCanAddUser = true;
mUserCapabilities.mUserSwitcherEnabled = false;
@@ -453,6 +455,7 @@ public class UserSettingsTest {
@Test
public void updateUserList_userSwitcherDisabled_shouldNotShowAddGuest() {
+ givenUsers(getAdminUser(true));
mUserCapabilities.mCanAddGuest = true;
mUserCapabilities.mUserSwitcherEnabled = false;
@@ -511,6 +514,47 @@ public class UserSettingsTest {
assertThat(userPref.getOnPreferenceClickListener()).isSameInstanceAs(mFragment);
}
+ @Test
+ public void updateUserList_existingSecondaryUser_shouldAddOnlyCurrUser_MultiUserIsDisabled() {
+ givenUsers(getAdminUser(true), getSecondaryUser(false));
+ mUserCapabilities.mUserSwitcherEnabled = false;
+
+ mFragment.updateUserList();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class);
+ verify(mFragment.mUserListCategory, times(1))
+ .addPreference(captor.capture());
+
+ List userPrefs = captor.getAllValues();
+ assertThat(userPrefs.size()).isEqualTo(1);
+ assertThat(userPrefs.get(0)).isSameInstanceAs(mMePreference);
+ }
+
+ @Test
+ public void updateUserList_existingSecondaryUser_shouldAddSecondaryUser_MultiUserIsEnabled() {
+ givenUsers(getAdminUser(true), getSecondaryUser(false));
+
+ mFragment.updateUserList();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class);
+ verify(mFragment.mUserListCategory, times(2))
+ .addPreference(captor.capture());
+
+ List userPrefs = captor.getAllValues();
+ UserPreference adminPref = userPrefs.get(0);
+ UserPreference secondaryPref = userPrefs.get(1);
+
+ assertThat(userPrefs.size()).isEqualTo(2);
+ assertThat(adminPref).isSameInstanceAs(mMePreference);
+ assertThat(secondaryPref.getUserId()).isEqualTo(INACTIVE_SECONDARY_USER_ID);
+ assertThat(secondaryPref.getTitle()).isEqualTo(SECONDARY_USER_NAME);
+ assertThat(secondaryPref.getIcon()).isNotNull();
+ assertThat(secondaryPref.getKey()).isEqualTo("id=" + INACTIVE_SECONDARY_USER_ID);
+ assertThat(secondaryPref.isEnabled()).isEqualTo(true);
+ assertThat(secondaryPref.isSelectable()).isEqualTo(true);
+ assertThat(secondaryPref.getOnPreferenceClickListener()).isSameInstanceAs(mFragment);
+ }
+
@Test
public void updateUserList_existingRestrictedUser_shouldAddRestrictedUserPreference() {
givenUsers(getAdminUser(true), getRestrictedUser(false));
@@ -800,6 +844,8 @@ public class UserSettingsTest {
List users = Arrays.asList(userInfo);
doReturn(users).when(mUserManager).getUsers();
doReturn(users).when(mUserManager).getAliveUsers();
+ users.forEach(user ->
+ doReturn(user).when(mUserManager).getUserInfo(user.id));
}
private static void removeFlag(UserInfo userInfo, int flag) {