1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.users; 18 19 import android.app.AppGlobals; 20 import android.appwidget.AppWidgetManager; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.IPackageManager; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.graphics.drawable.Drawable; 28 import android.os.RemoteException; 29 import android.os.UserHandle; 30 import android.text.TextUtils; 31 import android.util.ArraySet; 32 import android.util.Log; 33 34 import androidx.annotation.VisibleForTesting; 35 36 import java.util.ArrayList; 37 import java.util.Comparator; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Set; 41 42 /** 43 * Helper for {@link com.android.settings.users.AppCopyFragment}, for keeping track of which 44 * packages a user has chosen to copy to a second user and fulfilling that installation. 45 * 46 * To test, use 47 * atest SettingsLibTests:com.android.settingslib.users.AppCopyingHelperTest 48 */ 49 public class AppCopyHelper { 50 private static final boolean DEBUG = false; 51 private static final String TAG = "AppCopyHelper"; 52 53 private final PackageManager mPackageManager; 54 private final IPackageManager mIPm; 55 private final UserHandle mUser; 56 private boolean mLeanback; 57 58 /** Set of packages to be installed. */ 59 private final ArraySet<String> mSelectedPackages = new ArraySet<>(); 60 /** List of installable packages from which the user can choose. */ 61 private List<SelectableAppInfo> mVisibleApps; 62 AppCopyHelper(Context context, UserHandle user)63 public AppCopyHelper(Context context, UserHandle user) { 64 this(new Injector(context, user)); 65 } 66 67 @VisibleForTesting AppCopyHelper(Injector injector)68 AppCopyHelper(Injector injector) { 69 mPackageManager = injector.getPackageManager(); 70 mIPm = injector.getIPackageManager(); 71 mUser = injector.getUser(); 72 } 73 74 /** Toggles whether the package has been selected. */ setPackageSelected(String packageName, boolean selected)75 public void setPackageSelected(String packageName, boolean selected) { 76 if (selected) { 77 mSelectedPackages.add(packageName); 78 } else { 79 mSelectedPackages.remove(packageName); 80 } 81 } 82 83 /** Resets all packages as unselected. */ resetSelectedPackages()84 public void resetSelectedPackages() { 85 mSelectedPackages.clear(); 86 } 87 setLeanback(boolean isLeanback)88 public void setLeanback(boolean isLeanback) { 89 mLeanback = isLeanback; 90 } 91 92 /** List of installable packages from which the user can choose. */ getVisibleApps()93 public List<SelectableAppInfo> getVisibleApps() { 94 return mVisibleApps; 95 } 96 97 /** Installs the packages that have been selected using {@link #setPackageSelected} */ installSelectedApps()98 public void installSelectedApps() { 99 for (int i = 0; i < mSelectedPackages.size(); i++) { 100 final String packageName = mSelectedPackages.valueAt(i); 101 installSelectedApp(packageName); 102 } 103 } 104 installSelectedApp(String packageName)105 private void installSelectedApp(String packageName) { 106 final int userId = mUser.getIdentifier(); 107 try { 108 final ApplicationInfo info = mIPm.getApplicationInfo(packageName, 109 PackageManager.MATCH_ANY_USER, userId); 110 if (info == null || !info.enabled 111 || (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { 112 Log.i(TAG, "Installing " + packageName); 113 mIPm.installExistingPackageAsUser(packageName, mUser.getIdentifier(), 114 PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS, 115 PackageManager.INSTALL_REASON_UNKNOWN, null); 116 } 117 if (info != null && (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_HIDDEN) != 0 118 && (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0) { 119 Log.i(TAG, "Unhiding " + packageName); 120 mIPm.setApplicationHiddenSettingAsUser(packageName, false, userId); 121 } 122 } catch (RemoteException re) { 123 // Ignore 124 } 125 } 126 127 /** 128 * Fetches the list of installable packages to display. 129 * This list can be obtained from {@link #getVisibleApps}. 130 */ fetchAndMergeApps()131 public void fetchAndMergeApps() { 132 mVisibleApps = new ArrayList<>(); 133 addCurrentUsersApps(); 134 removeSecondUsersApp(); 135 } 136 137 /** 138 * Adds to {@link #mVisibleApps} packages from the current user: 139 * (1) All downloaded apps and 140 * (2) all system apps that have launcher or widgets. 141 */ addCurrentUsersApps()142 private void addCurrentUsersApps() { 143 // Add system package launchers of the current user 144 final Intent launcherIntent = new Intent(Intent.ACTION_MAIN).addCategory( 145 mLeanback ? Intent.CATEGORY_LEANBACK_LAUNCHER : Intent.CATEGORY_LAUNCHER); 146 addSystemApps(mVisibleApps, launcherIntent); 147 148 // Add system package widgets of the current user 149 final Intent widgetIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); 150 addSystemApps(mVisibleApps, widgetIntent); 151 152 // Add all downloaded apps of the current user 153 final List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications(0); 154 for (ApplicationInfo app : installedApps) { 155 // If it's not installed, skip 156 if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) continue; 157 158 if ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 159 && (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0) { 160 // Downloaded app 161 final SelectableAppInfo info = new SelectableAppInfo(); 162 info.packageName = app.packageName; 163 info.appName = app.loadLabel(mPackageManager); 164 info.icon = app.loadIcon(mPackageManager); 165 mVisibleApps.add(info); 166 } 167 } 168 169 // Remove dupes 170 final Set<String> dedupPackages = new HashSet<>(); 171 for (int i = mVisibleApps.size() - 1; i >= 0; i--) { 172 final SelectableAppInfo info = mVisibleApps.get(i); 173 if (DEBUG) Log.i(TAG, info.toString()); 174 if (!TextUtils.isEmpty(info.packageName) && dedupPackages.contains(info.packageName)) { 175 mVisibleApps.remove(i); 176 } else { 177 dedupPackages.add(info.packageName); 178 } 179 } 180 181 // Sort the list of visible apps 182 mVisibleApps.sort(new AppLabelComparator()); 183 } 184 185 /** Removes from {@link #mVisibleApps} all packages already installed on mUser. */ removeSecondUsersApp()186 private void removeSecondUsersApp() { 187 // Get the set of apps already installed for mUser 188 final Set<String> userPackages = new HashSet<>(); 189 final List<ApplicationInfo> userAppInfos = mPackageManager.getInstalledApplicationsAsUser( 190 PackageManager.MATCH_UNINSTALLED_PACKAGES, mUser.getIdentifier()); 191 for (int i = userAppInfos.size() - 1; i >= 0; i--) { 192 final ApplicationInfo app = userAppInfos.get(i); 193 if ((app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) continue; 194 userPackages.add(app.packageName); 195 } 196 197 for (int i = mVisibleApps.size() - 1; i >= 0; i--) { 198 final SelectableAppInfo info = mVisibleApps.get(i); 199 if (DEBUG) Log.i(TAG, info.toString()); 200 if (!TextUtils.isEmpty(info.packageName) && userPackages.contains(info.packageName)) { 201 mVisibleApps.remove(i); 202 } 203 } 204 } 205 206 /** 207 * Add system apps that match an intent to the list. 208 * @param visibleApps list of apps to append the new list to 209 * @param intent the intent to match 210 */ addSystemApps(List<SelectableAppInfo> visibleApps, Intent intent)211 private void addSystemApps(List<SelectableAppInfo> visibleApps, Intent intent) { 212 final List<ResolveInfo> intentApps = mPackageManager.queryIntentActivities(intent, 0); 213 for (ResolveInfo app : intentApps) { 214 if (app.activityInfo != null && app.activityInfo.applicationInfo != null) { 215 final int flags = app.activityInfo.applicationInfo.flags; 216 if ((flags & ApplicationInfo.FLAG_SYSTEM) != 0 217 || (flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { 218 219 final SelectableAppInfo info = new SelectableAppInfo(); 220 info.packageName = app.activityInfo.packageName; 221 info.appName = app.activityInfo.applicationInfo.loadLabel(mPackageManager); 222 info.icon = app.activityInfo.loadIcon(mPackageManager); 223 224 visibleApps.add(info); 225 } 226 } 227 } 228 } 229 230 /** Container for a package, its name, and icon. */ 231 public static class SelectableAppInfo { 232 public String packageName; 233 public CharSequence appName; 234 public Drawable icon; 235 236 @Override toString()237 public String toString() { 238 return packageName + ": appName=" + appName + "; icon=" + icon; 239 } 240 } 241 242 private static class AppLabelComparator implements Comparator<SelectableAppInfo> { 243 @Override compare(SelectableAppInfo lhs, SelectableAppInfo rhs)244 public int compare(SelectableAppInfo lhs, SelectableAppInfo rhs) { 245 String lhsLabel = lhs.appName.toString(); 246 String rhsLabel = rhs.appName.toString(); 247 return lhsLabel.toLowerCase().compareTo(rhsLabel.toLowerCase()); 248 } 249 } 250 251 /** 252 * Unit test will subclass it to inject mocks. 253 */ 254 @VisibleForTesting 255 static class Injector { 256 private final Context mContext; 257 private final UserHandle mUser; 258 Injector(Context context, UserHandle user)259 Injector(Context context, UserHandle user) { 260 mContext = context; 261 mUser = user; 262 } 263 getUser()264 UserHandle getUser() { 265 return mUser; 266 } 267 getPackageManager()268 PackageManager getPackageManager() { 269 return mContext.getPackageManager(); 270 } 271 getIPackageManager()272 IPackageManager getIPackageManager() { 273 return AppGlobals.getPackageManager(); 274 } 275 } 276 } 277