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
This commit is contained in:
Ankita Vyas
2022-12-13 09:49:56 +00:00
parent 23a91ff7d9
commit 3672fb4b85
7 changed files with 277 additions and 2 deletions

View File

@@ -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),

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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<ActionButton> = 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
}
}
}