diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AppCopyHelper.java b/packages/SettingsLib/src/com/android/settingslib/users/AppCopyHelper.java new file mode 100644 index 0000000000000..75e23e0abfbd9 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/users/AppCopyHelper.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2021 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.settingslib.users; + +import android.app.AppGlobals; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper for {@link com.android.settings.users.AppCopyFragment}, for keeping track of which + * packages a user has chosen to copy to a second user and fulfilling that installation. + * + * To test, use + * atest SettingsLibTests:com.android.settingslib.users.AppCopyingHelperTest + */ +public class AppCopyHelper { + private static final boolean DEBUG = false; + private static final String TAG = "AppCopyHelper"; + + private final PackageManager mPackageManager; + private final IPackageManager mIPm; + private final UserHandle mUser; + private boolean mLeanback; + + /** Set of packages to be installed. */ + private final ArraySet mSelectedPackages = new ArraySet<>(); + /** List of installable packages from which the user can choose. */ + private List mVisibleApps; + + public AppCopyHelper(Context context, UserHandle user) { + this(new Injector(context, user)); + } + + @VisibleForTesting + AppCopyHelper(Injector injector) { + mPackageManager = injector.getPackageManager(); + mIPm = injector.getIPackageManager(); + mUser = injector.getUser(); + } + + /** Toggles whether the package has been selected. */ + public void setPackageSelected(String packageName, boolean selected) { + if (selected) { + mSelectedPackages.add(packageName); + } else { + mSelectedPackages.remove(packageName); + } + } + + /** Resets all packages as unselected. */ + public void resetSelectedPackages() { + mSelectedPackages.clear(); + } + + public void setLeanback(boolean isLeanback) { + mLeanback = isLeanback; + } + + /** List of installable packages from which the user can choose. */ + public List getVisibleApps() { + return mVisibleApps; + } + + /** Installs the packages that have been selected using {@link #setPackageSelected} */ + public void installSelectedApps() { + for (int i = 0; i < mSelectedPackages.size(); i++) { + final String packageName = mSelectedPackages.valueAt(i); + installSelectedApp(packageName); + } + } + + private void installSelectedApp(String packageName) { + final int userId = mUser.getIdentifier(); + try { + final ApplicationInfo info = mIPm.getApplicationInfo(packageName, + PackageManager.MATCH_ANY_USER, userId); + if (info == null || !info.enabled + || (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { + Log.i(TAG, "Installing " + packageName); + mIPm.installExistingPackageAsUser(packageName, mUser.getIdentifier(), + PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS, + PackageManager.INSTALL_REASON_UNKNOWN, null); + } + if (info != null && (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_HIDDEN) != 0 + && (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0) { + Log.i(TAG, "Unhiding " + packageName); + mIPm.setApplicationHiddenSettingAsUser(packageName, false, userId); + } + } catch (RemoteException re) { + // Ignore + } + } + + /** + * Fetches the list of installable packages to display. + * This list can be obtained from {@link #getVisibleApps}. + */ + public void fetchAndMergeApps() { + mVisibleApps = new ArrayList<>(); + addCurrentUsersApps(); + removeSecondUsersApp(); + } + + /** + * Adds to {@link #mVisibleApps} packages from the current user: + * (1) All downloaded apps and + * (2) all system apps that have launcher or widgets. + */ + private void addCurrentUsersApps() { + // Add system package launchers of the current user + final Intent launcherIntent = new Intent(Intent.ACTION_MAIN).addCategory( + mLeanback ? Intent.CATEGORY_LEANBACK_LAUNCHER : Intent.CATEGORY_LAUNCHER); + addSystemApps(mVisibleApps, launcherIntent); + + // Add system package widgets of the current user + final Intent widgetIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + addSystemApps(mVisibleApps, widgetIntent); + + // Add all downloaded apps of the current user + final List installedApps = mPackageManager.getInstalledApplications(0); + for (ApplicationInfo app : installedApps) { + // If it's not installed, skip + if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) continue; + + if ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 + && (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) { + // Downloaded app + final SelectableAppInfo info = new SelectableAppInfo(); + info.packageName = app.packageName; + info.appName = app.loadLabel(mPackageManager); + info.icon = app.loadIcon(mPackageManager); + mVisibleApps.add(info); + } + } + + // Remove dupes + final Set dedupPackages = new HashSet<>(); + for (int i = mVisibleApps.size() - 1; i >= 0; i--) { + final SelectableAppInfo info = mVisibleApps.get(i); + if (DEBUG) Log.i(TAG, info.toString()); + if (!TextUtils.isEmpty(info.packageName) && dedupPackages.contains(info.packageName)) { + mVisibleApps.remove(i); + } else { + dedupPackages.add(info.packageName); + } + } + + // Sort the list of visible apps + mVisibleApps.sort(new AppLabelComparator()); + } + + /** Removes from {@link #mVisibleApps} all packages already installed on mUser. */ + private void removeSecondUsersApp() { + // Get the set of apps already installed for mUser + final Set userPackages = new HashSet<>(); + final List userAppInfos = mPackageManager.getInstalledApplicationsAsUser( + PackageManager.MATCH_UNINSTALLED_PACKAGES, mUser.getIdentifier()); + for (int i = userAppInfos.size() - 1; i >= 0; i--) { + final ApplicationInfo app = userAppInfos.get(i); + if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) continue; + userPackages.add(app.packageName); + } + + for (int i = mVisibleApps.size() - 1; i >= 0; i--) { + final SelectableAppInfo info = mVisibleApps.get(i); + if (DEBUG) Log.i(TAG, info.toString()); + if (!TextUtils.isEmpty(info.packageName) && userPackages.contains(info.packageName)) { + mVisibleApps.remove(i); + } + } + } + + /** + * Add system apps that match an intent to the list. + * @param visibleApps list of apps to append the new list to + * @param intent the intent to match + */ + private void addSystemApps(List visibleApps, Intent intent) { + final List intentApps = mPackageManager.queryIntentActivities(intent, 0); + for (ResolveInfo app : intentApps) { + if (app.activityInfo != null && app.activityInfo.applicationInfo != null) { + final int flags = app.activityInfo.applicationInfo.flags; + if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0 + || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + + final SelectableAppInfo info = new SelectableAppInfo(); + info.packageName = app.activityInfo.packageName; + info.appName = app.activityInfo.applicationInfo.loadLabel(mPackageManager); + info.icon = app.activityInfo.loadIcon(mPackageManager); + + visibleApps.add(info); + } + } + } + } + + /** Container for a package, its name, and icon. */ + public static class SelectableAppInfo { + public String packageName; + public CharSequence appName; + public Drawable icon; + + @Override + public String toString() { + return packageName + ": appName=" + appName + "; icon=" + icon; + } + } + + private static class AppLabelComparator implements Comparator { + @Override + public int compare(SelectableAppInfo lhs, SelectableAppInfo rhs) { + String lhsLabel = lhs.appName.toString(); + String rhsLabel = rhs.appName.toString(); + return lhsLabel.toLowerCase().compareTo(rhsLabel.toLowerCase()); + } + } + + /** + * Unit test will subclass it to inject mocks. + */ + @VisibleForTesting + static class Injector { + private final Context mContext; + private final UserHandle mUser; + + Injector(Context context, UserHandle user) { + mContext = context; + mUser = user; + } + + UserHandle getUser() { + return mUser; + } + + PackageManager getPackageManager() { + return mContext.getPackageManager(); + } + + IPackageManager getIPackageManager() { + return AppGlobals.getPackageManager(); + } + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppCopyingHelperTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppCopyingHelperTest.java new file mode 100644 index 0000000000000..111bcc46907ee --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppCopyingHelperTest.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2016 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.settingslib.users; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.UserHandle; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.ArraySet; + +import com.android.settingslib.BaseTest; + +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Tests for AppCopyHelper. + */ +@SmallTest +public class AppCopyingHelperTest extends BaseTest { + private @Mock Context mContext; + private @Mock PackageManager mPm; + private @Mock IPackageManager mIpm; + + private final UserHandle mTestUser = UserHandle.of(1111); + private AppCopyHelper mHelper; + + private final ArrayList mCurrUserInstalledAppInfos = new ArrayList<>(); + private final ArrayList mTestUserInstalledAppInfos = new ArrayList<>(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + mHelper = new AppCopyHelper(new TestInjector()); + } + + public void testFetchAndMergeApps() throws Exception { + // Apps on the current user. + final String[] sysInapplicables = new String[] {"sys.no0, sys.no1"}; + final String[] sysLaunchables = new String[] {"sys1", "sys2", "sys3"}; + final String[] sysWidgets = new String[] {"sys1", "sys4"}; + final String[] downloadeds = new String[] {"app1", "app2"}; + + addInapplicableSystemApps(sysInapplicables); + addSystemAppsForIntent(new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), + sysLaunchables); + addSystemAppsForIntent(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE), + sysWidgets); + addDownloadedApps(downloadeds); + when(mPm.getInstalledApplications(anyInt())).thenReturn(mCurrUserInstalledAppInfos); + + // Apps on the test user. + final String[] testUserApps = + new String[]{"sys.no0", "sys2", "sys4", "app2", "sys999", "app999"}; + addAppsToTestUser(testUserApps); + when(mPm.getInstalledApplicationsAsUser(anyInt(), eq(mTestUser.getIdentifier()))) + .thenReturn(mTestUserInstalledAppInfos); + + mHelper.fetchAndMergeApps(); + + final ArraySet notExpectedInVisibleApps = new ArraySet<>(); + Collections.addAll(notExpectedInVisibleApps, sysInapplicables); + Collections.addAll(notExpectedInVisibleApps, testUserApps); + + final ArraySet expectedInVisibleApps = new ArraySet<>(); + Collections.addAll(expectedInVisibleApps, sysLaunchables); + Collections.addAll(expectedInVisibleApps, sysWidgets); + Collections.addAll(expectedInVisibleApps, downloadeds); + expectedInVisibleApps.removeAll(notExpectedInVisibleApps); + + for (AppCopyHelper.SelectableAppInfo info : mHelper.getVisibleApps()) { + if (expectedInVisibleApps.contains(info.packageName)) { + expectedInVisibleApps.remove(info.packageName); + } else if (notExpectedInVisibleApps.contains(info.packageName)) { + fail("Package: " + info.packageName + " should not be included in visibleApps"); + } else { + fail("Unknown package: " + info.packageName); + } + } + assertEquals("Some expected apps are not included in visibleApps: " + expectedInVisibleApps, + 0, expectedInVisibleApps.size()); + } + + public void testInstallSelectedApps() throws Exception { + final int testUserId = mTestUser.getIdentifier(); + + mHelper.setPackageSelected("app1", true); // Ultimately true + mHelper.setPackageSelected("app2", true); // Ultimately false + mHelper.setPackageSelected("app3", true); // Ultimately true + mHelper.setPackageSelected("app4", true); // Ultimately true + + mHelper.setPackageSelected("app2", false); + mHelper.setPackageSelected("app1", false); + mHelper.setPackageSelected("app1", true); + + + // app3 is installed but hidden + ApplicationInfo info = new ApplicationInfo(); + info.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HIDDEN; + info.flags |= ApplicationInfo.FLAG_INSTALLED; + when(mIpm.getApplicationInfo(eq("app3"), anyInt(), eq(testUserId))) + .thenReturn(info); + + info = new ApplicationInfo(); + when(mIpm.getApplicationInfo(eq("app4"), anyInt(), eq(testUserId))) + .thenReturn(info); + + mHelper.installSelectedApps(); + + verify(mIpm, times(1)).installExistingPackageAsUser( + "app1", testUserId, + PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS, + PackageManager.INSTALL_REASON_UNKNOWN, null); + verify(mIpm, times(0)).installExistingPackageAsUser(eq( + "app2"), eq(testUserId), + anyInt(), anyInt(), any()); + verify(mIpm, times(0)).installExistingPackageAsUser(eq( + "app3"), eq(testUserId), + anyInt(), anyInt(), any()); + verify(mIpm, times(1)).installExistingPackageAsUser( + "app4", testUserId, + PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS, + PackageManager.INSTALL_REASON_UNKNOWN, null); + + verify(mIpm, times(0)).setApplicationHiddenSettingAsUser( + eq("app1"), anyBoolean(), eq(testUserId)); + verify(mIpm, times(0)).setApplicationHiddenSettingAsUser( + eq("app2"), anyBoolean(), eq(testUserId)); + verify(mIpm, times(1)).setApplicationHiddenSettingAsUser( + eq("app3"), eq(false), eq(testUserId)); + verify(mIpm, times(0)).setApplicationHiddenSettingAsUser( + eq("app4"), anyBoolean(), eq(testUserId)); + } + + private void addSystemAppsForIntent(Intent intent, String... packages) throws Exception { + final List resolveInfos = new ArrayList<>(); + for (String pkg : packages) { + final ResolveInfo ri = createResolveInfoForSystemApp(pkg); + resolveInfos.add(ri); + addInstalledApp(ri, false); + } + when(mPm.queryIntentActivities(argThat(new IntentMatcher(intent)), anyInt())) + .thenReturn(resolveInfos); + } + + private void addInapplicableSystemApps(String... packages) throws Exception { + for (String pkg : packages) { + final ResolveInfo ri = createResolveInfoForSystemApp(pkg); + addInstalledApp(ri, false); + } + } + + private void addDownloadedApps(String... packages) throws Exception { + for (String pkg : packages) { + final ResolveInfo ri = createResolveInfo(pkg); + addInstalledApp(ri, false); + } + } + + private void addAppsToTestUser(String... packages) throws Exception { + for (String pkg : packages) { + final ResolveInfo ri = createResolveInfo(pkg); + addInstalledApp(ri, true); + } + } + + private void addInstalledApp(ResolveInfo ri, boolean testUser) + throws PackageManager.NameNotFoundException { + final String pkgName = ri.activityInfo.packageName; + final PackageInfo packageInfo = new PackageInfo(); + packageInfo.applicationInfo = ri.activityInfo.applicationInfo; + packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED; + if (testUser) { + mTestUserInstalledAppInfos.add(packageInfo.applicationInfo); + } else { + mCurrUserInstalledAppInfos.add(packageInfo.applicationInfo); + } + when(mPm.getPackageInfo(eq(pkgName), anyInt())).thenReturn(packageInfo); + } + + private ResolveInfo createResolveInfoForSystemApp(String packageName) { + final ResolveInfo ri = createResolveInfo(packageName); + ri.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + ri.serviceInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + return ri; + } + + private ResolveInfo createResolveInfo(String packageName) { + final ResolveInfo ri = new ResolveInfo(); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName; + final ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.applicationInfo = applicationInfo; + activityInfo.packageName = packageName; + activityInfo.name = ""; + ri.activityInfo = activityInfo; + final ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.applicationInfo = applicationInfo; + serviceInfo.packageName = packageName; + serviceInfo.name = ""; + ri.serviceInfo = serviceInfo; + return ri; + } + + private static class IntentMatcher implements ArgumentMatcher { + private final Intent mIntent; + + IntentMatcher(Intent intent) { + mIntent = intent; + } + + @Override + public boolean matches(Intent argument) { + return argument != null && argument.filterEquals(mIntent); + } + + @Override + public String toString() { + return "Expected: " + mIntent; + } + } + + private class TestInjector extends AppCopyHelper.Injector { + TestInjector() { + super(mContext, mTestUser); + } + + @Override + UserHandle getUser() { + return mTestUser; + } + + @Override + PackageManager getPackageManager() { + return mPm; + } + + @Override + IPackageManager getIPackageManager() { + return mIpm; + } + } +}