From 5784198b5659784847c2729027a91ed9ad849248 Mon Sep 17 00:00:00 2001 From: Ankita Vyas Date: Tue, 29 Nov 2022 06:38:03 +0000 Subject: [PATCH] AppClone: Implement clone backend flow - Add onClick listeners of add/trash icons on Cloned Apps page - New layout with ImageView(Add icon) and ProgressBar - Creation of clone user and install package in clone user - Uninstallation of cloned app - Summary when app is being cloned and after clone completion - Action metrics Bug: 259022623 Test: make RunSettingsRoboTests -j64 Change-Id: Idc76fb8d88ba8987084beef2a0ce4c57d6c45b9e --- .../preference_widget_add_progressbar.xml | 41 +++++ res/values/strings.xml | 4 + .../AppStateClonedAppsBridge.java | 15 +- .../ApplicationViewHolder.java | 105 +++++++++-- .../manageapplications/CloneBackend.java | 163 ++++++++++++++++++ .../ManageApplications.java | 13 +- .../ApplicationViewHolderTest.java | 8 +- 7 files changed, 317 insertions(+), 32 deletions(-) create mode 100644 res/layout/preference_widget_add_progressbar.xml create mode 100644 src/com/android/settings/applications/manageapplications/CloneBackend.java diff --git a/res/layout/preference_widget_add_progressbar.xml b/res/layout/preference_widget_add_progressbar.xml new file mode 100644 index 00000000000..95ee09e44c3 --- /dev/null +++ b/res/layout/preference_widget_add_progressbar.xml @@ -0,0 +1,41 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 1bdac77b769..c12715de5b9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6468,6 +6468,10 @@ Create a second instance of an app so that you can use two accounts at the same time. %1$s cloned, %2$d available to clone + + Creating… + + Cloned Languages, gestures, time, backup diff --git a/src/com/android/settings/applications/AppStateClonedAppsBridge.java b/src/com/android/settings/applications/AppStateClonedAppsBridge.java index 7feaa3b3583..3348079b294 100644 --- a/src/com/android/settings/applications/AppStateClonedAppsBridge.java +++ b/src/com/android/settings/applications/AppStateClonedAppsBridge.java @@ -41,6 +41,7 @@ public class AppStateClonedAppsBridge extends AppStateBaseBridge{ private final Context mContext; private final List mAllowedApps; private List mCloneProfileApps = new ArrayList<>(); + private int mCloneUserId; public AppStateClonedAppsBridge(Context context, ApplicationsState appState, Callback callback) { @@ -48,17 +49,17 @@ public class AppStateClonedAppsBridge extends AppStateBaseBridge{ mContext = context; mAllowedApps = Arrays.asList(mContext.getResources() .getStringArray(com.android.internal.R.array.cloneable_apps)); - - int cloneUserId = Utils.getCloneUserId(mContext); - if (cloneUserId != -1) { - mCloneProfileApps = mContext.getPackageManager() - .getInstalledPackagesAsUser(GET_ACTIVITIES, - cloneUserId).stream().map(x -> x.packageName).toList(); - } } @Override protected void loadAllExtraInfo() { + mCloneUserId = Utils.getCloneUserId(mContext); + if (mCloneUserId != -1) { + mCloneProfileApps = mContext.getPackageManager() + .getInstalledPackagesAsUser(GET_ACTIVITIES, + mCloneUserId).stream().map(x -> x.packageName).toList(); + } + final List allApps = mAppSession.getAllApps(); for (int i = 0; i < allApps.size(); i++) { ApplicationsState.AppEntry app = allApps.get(i); diff --git a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java index f3f4b0ff847..744ac713914 100644 --- a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java +++ b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java @@ -16,28 +16,37 @@ package com.android.settings.applications.manageapplications; +import static com.android.settings.applications.manageapplications.ManageApplications.ApplicationsAdapter; import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_CLONED_APPS; +import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NONE; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.Switch; import android.widget.TextView; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentActivity; import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; +import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + public class ApplicationViewHolder extends RecyclerView.ViewHolder { @@ -51,8 +60,8 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { final ViewGroup mWidgetContainer; @VisibleForTesting final Switch mSwitch; - - private static int sListType; + final ImageView mAddIcon; + final ProgressBar mProgressBar; private final ImageView mAppIcon; @@ -64,33 +73,23 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { mDisabled = itemView.findViewById(R.id.appendix); mSwitch = itemView.findViewById(R.id.switchWidget); mWidgetContainer = itemView.findViewById(android.R.id.widget_frame); + mAddIcon = itemView.findViewById(R.id.add_preference_widget); + mProgressBar = itemView.findViewById(R.id.progressBar_cyclic); } static View newView(ViewGroup parent) { - return newView(parent, false /* twoTarget */); + return newView(parent, false /* twoTarget */, LIST_TYPE_NONE /* listType */); } - static View newView(ViewGroup parent , boolean twoTarget, int listType, Context context) { - sListType = listType; - return newView(parent, twoTarget); - } - - static View newView(ViewGroup parent, boolean twoTarget) { + static View newView(ViewGroup parent, boolean twoTarget, int listType) { ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()) .inflate(R.layout.preference_app, parent, false); - final ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame); + ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame); if (twoTarget) { if (widgetFrame != null) { - if (sListType == LIST_TYPE_CLONED_APPS) { + if (listType == LIST_TYPE_CLONED_APPS) { LayoutInflater.from(parent.getContext()) - .inflate(R.layout.preference_widget_add, widgetFrame, true); - //todo(b/259022623): Invoke the clone backend flow i.e. - // i) upon onclick of add icon, create new clone profile the first time - // and clone an app. - // ii) Show progress bar while app is being cloned - // iii) And upon onClick of trash icon, delete the cloned app instance - // from clone profile. - // iv) Log metrics + .inflate(R.layout.preference_widget_add_progressbar, widgetFrame, true); } else { LayoutInflater.from(parent.getContext()) .inflate(R.layout.preference_widget_primary_switch, widgetFrame, true); @@ -202,4 +201,72 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { mSwitch.setEnabled(enabled); } } + + void updateAppCloneWidget(Context context, View.OnClickListener onClickListener, + AppEntry entry) { + if (mAddIcon != null) { + if (!entry.isCloned) { + mAddIcon.setBackground(context.getDrawable(R.drawable.ic_add_24dp)); + } else { + mAddIcon.setBackground(context.getDrawable(R.drawable.ic_trash_can)); + setSummary(R.string.cloned_app_created_summary); + } + mAddIcon.setOnClickListener(onClickListener); + } + } + + View.OnClickListener appCloneOnClickListener(AppEntry entry, + ApplicationsAdapter adapter, FragmentActivity manageApplicationsActivity) { + Context context = manageApplicationsActivity.getApplicationContext(); + return new View.OnClickListener() { + @Override + public void onClick(View v) { + CloneBackend cloneBackend = CloneBackend.getInstance(context); + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + + String packageName = entry.info.packageName; + + if (mWidgetContainer != null) { + if (!entry.isCloned) { + metricsFeatureProvider.action(context, + SettingsEnums.ACTION_CREATE_CLONE_APP); + mAddIcon.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.VISIBLE); + setSummary(R.string.cloned_app_creation_summary); + + // todo(b/262352524): To figure out a way to prevent memory leak + // without making this static. + new AsyncTask(){ + + @Override + protected Integer doInBackground(Void... unused) { + return cloneBackend.installCloneApp(packageName); + } + + @Override + protected void onPostExecute(Integer res) { + mProgressBar.setVisibility(View.INVISIBLE); + mAddIcon.setVisibility(View.VISIBLE); + + if (res != CloneBackend.SUCCESS) { + setSummary(null); + return; + } + + // Refresh the page to reflect newly created cloned app. + adapter.rebuild(); + } + }.execute(); + + } else if (entry.isCloned) { + metricsFeatureProvider.action(context, + SettingsEnums.ACTION_DELETE_CLONE_APP); + cloneBackend.uninstallClonedApp(packageName, /*allUsers*/ false, + manageApplicationsActivity); + } + } + } + }; + } } diff --git a/src/com/android/settings/applications/manageapplications/CloneBackend.java b/src/com/android/settings/applications/manageapplications/CloneBackend.java new file mode 100644 index 00000000000..3365b510b6a --- /dev/null +++ b/src/com/android/settings/applications/manageapplications/CloneBackend.java @@ -0,0 +1,163 @@ +/* + * 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.applications.manageapplications; + +import static android.content.pm.PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; +import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_URI; +import static android.content.pm.PackageManager.INSTALL_REASON_USER; +import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; + +import android.app.ActivityManagerNative; +import android.app.AppGlobals; +import android.app.IActivityManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.Utils; + +import java.util.HashSet; + +/** + * Handles clone user creation and clone app install/uninstall. + */ +public class CloneBackend { + + public static final String TAG = "CloneBackend"; + public static final int SUCCESS = 0; + private static final int ERROR_CREATING_CLONE_USER = 1; + private static final int ERROR_STARTING_CLONE_USER = 2; + private static final int ERROR_CLONING_PACKAGE = 3; + private static CloneBackend sInstance; + private Context mContext; + private int mCloneUserId; + + private CloneBackend(Context context) { + mContext = context; + mCloneUserId = Utils.getCloneUserId(context); + } + + /** + * @param context + * @return a CloneBackend object + */ + public static CloneBackend getInstance(Context context) { + if (sInstance == null) { + sInstance = new CloneBackend(context); + } + return sInstance; + } + + /** + * Starts activity to uninstall cloned app. + * + *

Invokes {@link com.android.packageinstaller.UninstallerActivity} which then displays the + * dialog to the user and handles actual uninstall. + */ + void uninstallClonedApp(String packageName, boolean allUsers, FragmentActivity activity) { + // Create new intent to launch Uninstaller activity + Uri packageUri = Uri.parse("package:" + packageName); + Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); + uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); + uninstallIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(mCloneUserId)); + activity.startActivityForResult(uninstallIntent, 0); + } + + /** + * Installs another instance of given package in clone user. + * + *

Creates clone user if doesn't exist and starts the new user before installing app. + * @param packageName + * @return error/success code + */ + int installCloneApp(String packageName) { + String userName = "cloneUser"; + UserHandle cloneUserHandle = null; + boolean newlyCreated = false; + + // Create clone user if not already exists. + if (mCloneUserId == -1) { + UserManager um = mContext.getSystemService(UserManager.class); + try { + cloneUserHandle = um.createProfile(userName, USER_TYPE_PROFILE_CLONE, + new HashSet<>()); + } catch (Exception e) { + if (ManageApplications.DEBUG) { + Log.e("ankita", "Error occurred creating clone user" + e.getMessage()); + } + return ERROR_CREATING_CLONE_USER; + } + + if (cloneUserHandle != null) { + mCloneUserId = cloneUserHandle.getIdentifier(); + newlyCreated = true; + if (ManageApplications.DEBUG) { + Log.d(TAG, "Created clone user " + mCloneUserId); + } + } else { + mCloneUserId = -1; + } + } + + if (mCloneUserId > 0) { + // If clone user is newly created for the first time, then start this user. + if (newlyCreated) { + IActivityManager am = ActivityManagerNative.getDefault(); + try { + am.startUserInBackground(mCloneUserId); + } catch (RemoteException e) { + if (ManageApplications.DEBUG) { + Log.e(TAG, "Error starting clone user " + e.getMessage()); + } + return ERROR_STARTING_CLONE_USER; + } + } + + // Install given app in clone user + int res = 0; + try { + res = AppGlobals.getPackageManager().installExistingPackageAsUser( + packageName, mCloneUserId, + INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS, INSTALL_REASON_USER, null); + } catch (RemoteException e) { + if (ManageApplications.DEBUG) { + Log.e(TAG, "Error installing package" + packageName + " in clone user." + + e.getMessage()); + } + return ERROR_CLONING_PACKAGE; + } + + if (res == INSTALL_FAILED_INVALID_URI) { + if (ManageApplications.DEBUG) { + Log.e(TAG, "Package " + packageName + " doesn't exist."); + } + return ERROR_CLONING_PACKAGE; + } + } + + if (ManageApplications.DEBUG) { + Log.i(TAG, "Package " + packageName + " cloned successfully."); + } + return SUCCESS; + } +} diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index ce92459e977..e6c174c3dba 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -236,6 +236,7 @@ public class ManageApplications extends InstrumentedFragment private Menu mOptionsMenu; + public static final int LIST_TYPE_NONE = -1; public static final int LIST_TYPE_MAIN = 0; public static final int LIST_TYPE_NOTIFICATION = 1; public static final int LIST_TYPE_STORAGE = 3; @@ -1324,7 +1325,8 @@ public class ManageApplications extends InstrumentedFragment view = ApplicationViewHolder.newHeader(parent, R.string.desc_app_locale_selection_supported); } else if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { - view = ApplicationViewHolder.newView(parent, true /* twoTarget */); + view = ApplicationViewHolder.newView(parent, true /* twoTarget */, + LIST_TYPE_NOTIFICATION); } else if (mManageApplications.mListType == LIST_TYPE_CLONED_APPS && viewType == VIEW_TYPE_APP_HEADER) { view = ApplicationViewHolder.newHeader(parent, @@ -1332,9 +1334,10 @@ public class ManageApplications extends InstrumentedFragment } else if (mManageApplications.mListType == LIST_TYPE_CLONED_APPS && viewType == VIEW_TYPE_TWO_TARGET) { view = ApplicationViewHolder.newView( - parent, true, LIST_TYPE_CLONED_APPS, mContext); + parent, true, LIST_TYPE_CLONED_APPS); } else { - view = ApplicationViewHolder.newView(parent, false /* twoTarget */); + view = ApplicationViewHolder.newView(parent, false /* twoTarget */, + mManageApplications.mListType); } return new ApplicationViewHolder(view); } @@ -1781,7 +1784,9 @@ public class ManageApplications extends InstrumentedFragment } break; case LIST_TYPE_CLONED_APPS: - //todo(b/259022623): Attach onClick listener here. + holder.updateAppCloneWidget(mContext, + holder.appCloneOnClickListener(entry, this, + mManageApplications.getActivity()), entry); break; } } diff --git a/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java b/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java index be01a8acb02..1311fe23a9f 100644 --- a/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java +++ b/tests/robotests/src/com/android/settings/applications/manageapplications/ApplicationViewHolderTest.java @@ -16,6 +16,8 @@ package com.android.settings.applications.manageapplications; +import static com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NONE; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; @@ -117,7 +119,8 @@ public class ApplicationViewHolderTest { @Test public void twoTouchTarget() { - mView = ApplicationViewHolder.newView(new FrameLayout(mContext), true); + mView = ApplicationViewHolder.newView(new FrameLayout(mContext), true, + LIST_TYPE_NONE); mHolder = new ApplicationViewHolder(mView); assertThat(mHolder.mSwitch).isNotNull(); assertThat(mHolder.mWidgetContainer.getChildCount()).isEqualTo(1); @@ -126,7 +129,8 @@ public class ApplicationViewHolderTest { @Test public void updateSwitch() { final CountDownLatch latch = new CountDownLatch(1); - mView = ApplicationViewHolder.newView(new FrameLayout(mContext), true); + mView = ApplicationViewHolder.newView(new FrameLayout(mContext), true, + LIST_TYPE_NONE); mHolder = new ApplicationViewHolder(mView); mHolder.updateSwitch((buttonView, isChecked) -> latch.countDown(), true, true);