Merge "AppClone: Changes in AppInfo page for cloned app."

This commit is contained in:
Ankita Vyas
2023-01-12 05:04:43 +00:00
committed by Android (Google) Code Review
7 changed files with 277 additions and 2 deletions

View File

@@ -90,7 +90,7 @@ public class CloneBackend {
* @param packageName * @param packageName
* @return error/success code * @return error/success code
*/ */
int installCloneApp(String packageName) { public int installCloneApp(String packageName) {
String userName = "cloneUser"; String userName = "cloneUser";
UserHandle cloneUserHandle = null; UserHandle cloneUserHandle = null;
boolean newlyCreated = false; boolean newlyCreated = false;
@@ -160,4 +160,8 @@ public class CloneBackend {
} }
return SUCCESS; return SUCCESS;
} }
public int getCloneUserId() {
return mCloneUserId;
}
} }

View File

@@ -16,6 +16,8 @@
package com.android.settings.applications.manageapplications; 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_DRAGGING;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
@@ -47,6 +49,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageItemInfo; import android.content.pm.PackageItemInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
@@ -64,6 +67,7 @@ import android.provider.DeviceConfig;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.FeatureFlagUtils;
import android.util.IconDrawableFactory; import android.util.IconDrawableFactory;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -148,6 +152,8 @@ import com.android.settings.notification.ConfigureNotificationSettings;
import com.android.settings.notification.NotificationBackend; import com.android.settings.notification.NotificationBackend;
import com.android.settings.notification.app.AppNotificationSettings; import com.android.settings.notification.app.AppNotificationSettings;
import com.android.settings.spa.SpaActivity; 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.widget.LoadingViewController;
import com.android.settings.wifi.AppStateChangeWifiStateBridge; import com.android.settings.wifi.AppStateChangeWifiStateBridge;
import com.android.settings.wifi.ChangeWifiStateDetails; import com.android.settings.wifi.ChangeWifiStateDetails;
@@ -707,6 +713,20 @@ public class ManageApplications extends InstrumentedFragment
startAppInfoFragment(LongBackgroundTasksDetails.class, startAppInfoFragment(LongBackgroundTasksDetails.class,
R.string.long_background_tasks_label); R.string.long_background_tasks_label);
break; 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 // 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 // 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. // app. Maybe when they load the list of apps that contains managed profile apps.

View File

@@ -20,6 +20,7 @@ import android.content.Context
import com.android.settings.spa.app.AllAppListPageProvider import com.android.settings.spa.app.AllAppListPageProvider
import com.android.settings.spa.app.AppsMainPageProvider import com.android.settings.spa.app.AppsMainPageProvider
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider 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.backgroundinstall.BackgroundInstalledAppsPageProvider
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
@@ -72,6 +73,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
AppLanguagesPageProvider, AppLanguagesPageProvider,
UsageStatsPageProvider, UsageStatsPageProvider,
BackgroundInstalledAppsPageProvider, BackgroundInstalledAppsPageProvider,
CloneAppInfoSettingsProvider,
) + togglePermissionAppListTemplate.createPageProviders(), ) + togglePermissionAppListTemplate.createPageProviders(),
rootPages = listOf( rootPages = listOf(
SettingsPage.create(HomePageProvider.name), 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.om.OverlayManager
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.os.UserHandle
import android.os.UserManager
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import com.android.settings.R import com.android.settings.R
@@ -30,6 +32,7 @@ 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 overlayManager = context.getSystemService(OverlayManager::class.java)!!
private val userManager = context.getSystemService(UserManager::class.java)!!
fun getActionButton(app: ApplicationInfo): ActionButton? { fun getActionButton(app: ApplicationInfo): ActionButton? {
if (app.isSystemApp || app.isInstantApp) return null if (app.isSystemApp || app.isInstantApp) return null
@@ -80,7 +83,8 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
private fun uninstallButton(app: ApplicationInfo, enabled: Boolean) = ActionButton( 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, imageVector = Icons.Outlined.Delete,
enabled = enabled, enabled = enabled,
) { onUninstallClicked(app) } ) { onUninstallClicked(app) }
@@ -89,4 +93,9 @@ class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter)
if (appButtonRepository.isUninstallBlockedByAdmin(app)) return if (appButtonRepository.isUninstallBlockedByAdmin(app)) return
packageInfoPresenter.startUninstallActivity() 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
}
}
}