From 3672fb4b8576c81bfd6726e9639b01f8cfe6b7a7 Mon Sep 17 00:00:00 2001 From: Ankita Vyas Date: Tue, 13 Dec 2022 09:49:56 +0000 Subject: [PATCH] AppClone: Changes in AppInfo page for cloned app. - Hides preferences for cloneable apps under Cloned Apps page - Displays Create option for cloneable apps under Cloned Apps page. - Invokes CloneBackend on click of create and refreshes to display newly cloned app's AppInfo page. - Appends suffix 'clone' for cloneable/cloned app. - Displays text 'Delete' instead of 'uninstall'. Screencast: https://screencast.googleplex.com/cast/NjI3MDEyMjk1MzAxNTI5NnxhOTIxZDhiZC03Zg Bug: 262375058 Test: make RunSettingsRoboTests -j64 Change-Id: I34018f6cc7420d2667c25fbca59c832b398d723e --- .../manageapplications/CloneBackend.java | 6 +- .../ManageApplications.java | 20 +++++ .../settings/spa/SettingsSpaEnvironment.kt | 2 + .../spa/app/appinfo/AppCreateButton.kt | 74 ++++++++++++++++ .../spa/app/appinfo/AppUninstallButton.kt | 11 ++- .../spa/app/appinfo/CloneAppInfoSettings.kt | 82 ++++++++++++++++++ .../spa/app/appinfo/ClonePageAppButtons.kt | 84 +++++++++++++++++++ 7 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/com/android/settings/spa/app/appinfo/AppCreateButton.kt create mode 100644 src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt create mode 100644 src/com/android/settings/spa/app/appinfo/ClonePageAppButtons.kt diff --git a/src/com/android/settings/applications/manageapplications/CloneBackend.java b/src/com/android/settings/applications/manageapplications/CloneBackend.java index 3365b510b6a..64642535d99 100644 --- a/src/com/android/settings/applications/manageapplications/CloneBackend.java +++ b/src/com/android/settings/applications/manageapplications/CloneBackend.java @@ -90,7 +90,7 @@ public class CloneBackend { * @param packageName * @return error/success code */ - int installCloneApp(String packageName) { + public int installCloneApp(String packageName) { String userName = "cloneUser"; UserHandle cloneUserHandle = null; boolean newlyCreated = false; @@ -160,4 +160,8 @@ public class CloneBackend { } return SUCCESS; } + + public int getCloneUserId() { + return mCloneUserId; + } } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index e6c174c3dba..da832659cfa 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -16,6 +16,8 @@ package com.android.settings.applications.manageapplications; +import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_SPA; + import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; @@ -46,6 +48,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageItemInfo; +import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -61,6 +64,7 @@ import android.preference.PreferenceFrameLayout; import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; +import android.util.FeatureFlagUtils; import android.util.IconDrawableFactory; import android.util.Log; import android.view.LayoutInflater; @@ -144,6 +148,8 @@ import com.android.settings.notification.ConfigureNotificationSettings; import com.android.settings.notification.NotificationBackend; import com.android.settings.notification.app.AppNotificationSettings; import com.android.settings.spa.SpaActivity; +import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider; +import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider; import com.android.settings.widget.LoadingViewController; import com.android.settings.wifi.AppStateChangeWifiStateBridge; import com.android.settings.wifi.ChangeWifiStateDetails; @@ -703,6 +709,20 @@ public class ManageApplications extends InstrumentedFragment startAppInfoFragment(LongBackgroundTasksDetails.class, R.string.long_background_tasks_label); break; + case LIST_TYPE_CLONED_APPS: + if (!FeatureFlagUtils.isEnabled(getContext(), SETTINGS_ENABLE_SPA)) { + return; + } + int userId = UserHandle.getUserId(mCurrentUid); + UserInfo userInfo = mUserManager.getUserInfo(userId); + if (userInfo != null && !userInfo.isCloneProfile()) { + SpaActivity.startSpaActivity(getContext(), CloneAppInfoSettingsProvider.INSTANCE + .getRoute(mCurrentPkgName, userId)); + } else { + SpaActivity.startSpaActivity(getContext(), AppInfoSettingsProvider.INSTANCE + .getRoute(mCurrentPkgName, userId)); + } + break; // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed // app. Maybe when they load the list of apps that contains managed profile apps. diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index 9eab400326a..104e42f9fe9 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -20,6 +20,7 @@ import android.content.Context import com.android.settings.spa.app.AllAppListPageProvider import com.android.settings.spa.app.AppsMainPageProvider import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider +import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider import com.android.settings.spa.app.backgroundinstall.BackgroundInstalledAppsPageProvider import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider @@ -68,6 +69,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { AppLanguagesPageProvider, UsageStatsPageProvider, BackgroundInstalledAppsPageProvider, + CloneAppInfoSettingsProvider, ) + togglePermissionAppListTemplate.createPageProviders(), rootPages = listOf( SettingsPage.create(HomePageProvider.name), diff --git a/src/com/android/settings/spa/app/appinfo/AppCreateButton.kt b/src/com/android/settings/spa/app/appinfo/AppCreateButton.kt new file mode 100644 index 00000000000..db835764d51 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/AppCreateButton.kt @@ -0,0 +1,74 @@ +/* + * 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.appinfo + +import android.app.Activity +import android.app.settings.SettingsEnums +import android.content.pm.ApplicationInfo +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import com.android.settings.R +import com.android.settings.applications.manageapplications.CloneBackend +import com.android.settings.overlay.FeatureFactory +import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider.getRoute +import com.android.settingslib.spa.framework.compose.LocalNavController +import com.android.settingslib.spa.widget.button.ActionButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class AppCreateButton(packageInfoPresenter: PackageInfoPresenter) { + private val context = packageInfoPresenter.context + val enabledState = mutableStateOf(true) + + @Composable + fun getActionButton(app: ApplicationInfo): ActionButton? { + return createButton(app) + } + + @Composable + private fun createButton(app: ApplicationInfo): ActionButton { + val coroutineScope = rememberCoroutineScope() + val navController = LocalNavController.current + return ActionButton( + text = context.getString(R.string.create), + imageVector = Icons.Outlined.Add, + enabled = enabledState.value, + ) + { + val cloneBackend = CloneBackend.getInstance(context) + FeatureFactory.getFactory(context).metricsFeatureProvider.action(context, + SettingsEnums.ACTION_CREATE_CLONE_APP) + coroutineScope.launch { + enabledState.value = false + val result = installCloneApp(app, cloneBackend) + if (result == CloneBackend.SUCCESS) { + navController.navigate(getRoute(app.packageName, cloneBackend.cloneUserId)) + } else { + enabledState.value = true + } + } + } + } + + private suspend fun installCloneApp(app: ApplicationInfo, cloneBackend: CloneBackend): Int = withContext(Dispatchers.IO) { + cloneBackend.installCloneApp(app.packageName) + } +} \ No newline at end of file diff --git a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt index f05d611984f..01c60f85e60 100644 --- a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt +++ b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt @@ -18,6 +18,8 @@ package com.android.settings.spa.app.appinfo import android.content.om.OverlayManager import android.content.pm.ApplicationInfo +import android.os.UserHandle +import android.os.UserManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import com.android.settings.R @@ -30,6 +32,7 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) private val context = packageInfoPresenter.context private val appButtonRepository = AppButtonRepository(context) private val overlayManager = context.getSystemService(OverlayManager::class.java)!! + private val userManager = context.getSystemService(UserManager::class.java)!! fun getActionButton(app: ApplicationInfo): ActionButton? { if (app.isSystemApp || app.isInstantApp) return null @@ -80,7 +83,8 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true private fun uninstallButton(app: ApplicationInfo, enabled: Boolean) = ActionButton( - text = context.getString(R.string.uninstall_text), + text = if (isCloneApp(app)) context.getString(R.string.delete) else + context.getString(R.string.uninstall_text), imageVector = Icons.Outlined.Delete, enabled = enabled, ) { onUninstallClicked(app) } @@ -89,4 +93,9 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) if (appButtonRepository.isUninstallBlockedByAdmin(app)) return packageInfoPresenter.startUninstallActivity() } + + private fun isCloneApp(app: ApplicationInfo): Boolean { + val userInfo = userManager.getUserInfo(UserHandle.getUserId(app.uid)) + return userInfo != null && userInfo.isCloneProfile + } } diff --git a/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt new file mode 100644 index 00000000000..6f51689c2a6 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/CloneAppInfoSettings.kt @@ -0,0 +1,82 @@ +/* + * 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.appinfo + +import android.app.settings.SettingsEnums +import android.content.pm.ApplicationInfo +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 +import com.android.settings.R +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spaprivileged.model.app.toRoute +import com.android.settingslib.spaprivileged.template.app.AppInfoProvider + +private const val PACKAGE_NAME = "packageName" +private const val USER_ID = "userId" + +object CloneAppInfoSettingsProvider : SettingsPageProvider { + override val name = "CloneAppInfoSettingsProvider" + + override val parameter = listOf( + navArgument(PACKAGE_NAME) { type = NavType.StringType }, + navArgument(USER_ID) { type = NavType.IntType }, + ) + + @Composable + override fun Page(arguments: Bundle?) { + val packageName = arguments!!.getString(PACKAGE_NAME)!! + val userId = arguments.getInt(USER_ID) + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val packageInfoPresenter = remember { + PackageInfoPresenter(context, packageName, userId, coroutineScope) + } + CloneAppInfoSettings(packageInfoPresenter) + packageInfoPresenter.PackageRemoveDetector() + } + + @Composable + fun navigator(app: ApplicationInfo) = com.android.settingslib.spa.framework.compose.navigator(route = "$name/${app.toRoute()}") + + /** + * Gets the route to the App Info Settings page. + * + * Expose route to enable enter from non-SPA pages. + */ + fun getRoute(packageName: String, userId: Int): String = "$name/$packageName/$userId" +} + +@Composable +private fun CloneAppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { + val packageInfo = packageInfoPresenter.flow.collectAsState().value ?: return + RegularScaffold( + title = stringResource(R.string.application_info_label), + ) { + val appInfoProvider = remember { AppInfoProvider(packageInfo) } + + appInfoProvider.AppInfo(isClonedAppPage = true) + ClonePageAppButtons(packageInfoPresenter) + } +} diff --git a/src/com/android/settings/spa/app/appinfo/ClonePageAppButtons.kt b/src/com/android/settings/spa/app/appinfo/ClonePageAppButtons.kt new file mode 100644 index 00000000000..0fb437393b0 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/ClonePageAppButtons.kt @@ -0,0 +1,84 @@ +/* + * 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.appinfo + +import android.content.pm.ApplicationInfo +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Launch +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.settings.R +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.button.ActionButtons + +@Composable +fun ClonePageAppButtons(packageInfoPresenter: PackageInfoPresenter) { + val presenter = remember { CloneAppButtonsPresenter(packageInfoPresenter) } + ActionButtons(actionButtons = presenter.getActionButtons()) +} + +private class CloneAppButtonsPresenter(private val packageInfoPresenter: PackageInfoPresenter) { + private val appLaunchButton = FakeAppLaunchButton(packageInfoPresenter) + private val appCreateButton = AppCreateButton(packageInfoPresenter) + private val appForceStopButton = FakeAppForceStopButton(packageInfoPresenter) + + @OptIn(ExperimentalLifecycleComposeApi::class) + @Composable + fun getActionButtons() = + packageInfoPresenter.flow.collectAsStateWithLifecycle(initialValue = null).value?.let { + getActionButtons(it.applicationInfo) + } ?: emptyList() + + @Composable + private fun getActionButtons(app: ApplicationInfo): List = listOfNotNull( + appLaunchButton.getActionButton(), + appCreateButton.getActionButton(app), + appForceStopButton.getActionButton(), + ) +} + +class FakeAppForceStopButton(packageInfoPresenter: PackageInfoPresenter) { + private val context = packageInfoPresenter.context + + fun getActionButton(): ActionButton { + return ActionButton( + text = context.getString(R.string.force_stop), + imageVector = Icons.Outlined.WarningAmber, + enabled = false, + ) { + // Unclickable + } + } +} + +class FakeAppLaunchButton(packageInfoPresenter: PackageInfoPresenter) { + private val context = packageInfoPresenter.context + + @Composable + fun getActionButton(): ActionButton { + return ActionButton( + text = context.getString(R.string.launch_instant_app), + imageVector = Icons.Outlined.Launch, + enabled = false + ) { + // Unclickable + } + } +}