1 /* 2 * Copyright (C) 2008 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.launcher3.model; 18 19 import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR; 20 import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY; 21 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.LauncherActivityInfo; 26 import android.content.pm.LauncherApps; 27 import android.os.LocaleList; 28 import android.os.UserHandle; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 34 import com.android.launcher3.AppFilter; 35 import com.android.launcher3.compat.AlphabeticIndexCompat; 36 import com.android.launcher3.icons.IconCache; 37 import com.android.launcher3.model.BgDataModel.Callbacks; 38 import com.android.launcher3.model.data.AppInfo; 39 import com.android.launcher3.model.data.ItemInfo; 40 import com.android.launcher3.pm.PackageInstallInfo; 41 import com.android.launcher3.pm.UserCache; 42 import com.android.launcher3.util.ApiWrapper; 43 import com.android.launcher3.util.FlagOp; 44 import com.android.launcher3.util.PackageManagerHelper; 45 import com.android.launcher3.util.SafeCloseable; 46 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.function.Consumer; 52 import java.util.function.Predicate; 53 54 55 /** 56 * Stores the list of all applications for the all apps view. 57 */ 58 @SuppressWarnings("NewApi") 59 public class AllAppsList { 60 61 private static final String TAG = "AllAppsList"; 62 private static final Consumer<AppInfo> NO_OP_CONSUMER = a -> { }; 63 private static final boolean DEBUG = true; 64 65 public static final int DEFAULT_APPLICATIONS_NUMBER = 42; 66 67 /** The list off all apps. */ 68 public final ArrayList<AppInfo> data = new ArrayList<>(DEFAULT_APPLICATIONS_NUMBER); 69 70 @NonNull 71 private IconCache mIconCache; 72 73 @NonNull 74 private AppFilter mAppFilter; 75 76 private boolean mDataChanged = false; 77 private Consumer<AppInfo> mRemoveListener = NO_OP_CONSUMER; 78 79 private AlphabeticIndexCompat mIndex; 80 81 /** 82 * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION 83 * @see Callbacks#FLAG_QUIET_MODE_ENABLED 84 * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION 85 * @see Callbacks#FLAG_WORK_PROFILE_QUIET_MODE_ENABLED 86 * @see Callbacks#FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED 87 */ 88 private int mFlags; 89 90 /** 91 * Boring constructor. 92 */ AllAppsList(IconCache iconCache, AppFilter appFilter)93 public AllAppsList(IconCache iconCache, AppFilter appFilter) { 94 mIconCache = iconCache; 95 mAppFilter = appFilter; 96 mIndex = new AlphabeticIndexCompat(LocaleList.getDefault()); 97 } 98 99 /** 100 * Returns true if there have been any changes since last call. 101 */ getAndResetChangeFlag()102 public boolean getAndResetChangeFlag() { 103 boolean result = mDataChanged; 104 mDataChanged = false; 105 return result; 106 } 107 108 /** 109 * Helper to checking {@link Callbacks#FLAG_HAS_SHORTCUT_PERMISSION} 110 */ hasShortcutHostPermission()111 public boolean hasShortcutHostPermission() { 112 return (mFlags & Callbacks.FLAG_HAS_SHORTCUT_PERMISSION) != 0; 113 } 114 115 /** 116 * Sets or clears the provided flag 117 */ setFlags(int flagMask, boolean enabled)118 public void setFlags(int flagMask, boolean enabled) { 119 if (enabled) { 120 mFlags |= flagMask; 121 } else { 122 mFlags &= ~flagMask; 123 } 124 mDataChanged = true; 125 } 126 127 /** 128 * Returns the model flags 129 */ getFlags()130 public int getFlags() { 131 return mFlags; 132 } 133 134 135 /** 136 * Add the supplied ApplicationInfo objects to the list, and enqueue it into the 137 * list to broadcast when notify() is called. 138 * 139 * If the app is already in the list, doesn't add it. 140 */ add(AppInfo info, LauncherActivityInfo activityInfo)141 public void add(AppInfo info, LauncherActivityInfo activityInfo) { 142 add(info, activityInfo, true); 143 } 144 add(AppInfo info, LauncherActivityInfo activityInfo, boolean loadIcon)145 public void add(AppInfo info, LauncherActivityInfo activityInfo, boolean loadIcon) { 146 if (!mAppFilter.shouldShowApp(info.componentName)) { 147 return; 148 } 149 if (findAppInfo(info.componentName, info.user) != null) { 150 return; 151 } 152 if (loadIcon) { 153 mIconCache.getTitleAndIcon(info, activityInfo, false /* useLowResIcon */); 154 info.sectionName = mIndex.computeSectionName(info.title); 155 } else { 156 info.title = ""; 157 } 158 159 data.add(info); 160 mDataChanged = true; 161 } 162 163 @Nullable addPromiseApp(Context context, PackageInstallInfo installInfo)164 public AppInfo addPromiseApp(Context context, PackageInstallInfo installInfo) { 165 return addPromiseApp(context, installInfo, true); 166 } 167 168 @Nullable addPromiseApp( Context context, PackageInstallInfo installInfo, boolean loadIcon)169 public AppInfo addPromiseApp( 170 Context context, PackageInstallInfo installInfo, boolean loadIcon) { 171 // only if not yet installed 172 if (PackageManagerHelper.INSTANCE.get(context) 173 .isAppInstalled(installInfo.packageName, installInfo.user)) { 174 return null; 175 } 176 AppInfo promiseAppInfo = new AppInfo(installInfo); 177 178 if (loadIcon) { 179 mIconCache.getTitleAndIcon(promiseAppInfo, promiseAppInfo.usingLowResIcon()); 180 promiseAppInfo.sectionName = mIndex.computeSectionName(promiseAppInfo.title); 181 } else { 182 promiseAppInfo.title = ""; 183 } 184 185 data.add(promiseAppInfo); 186 mDataChanged = true; 187 188 return promiseAppInfo; 189 } 190 updateSectionName(AppInfo appInfo)191 public void updateSectionName(AppInfo appInfo) { 192 appInfo.sectionName = mIndex.computeSectionName(appInfo.title); 193 194 } 195 196 /** Updates the given PackageInstallInfo's associated AppInfo's installation info. */ updatePromiseInstallInfo(PackageInstallInfo installInfo)197 public List<AppInfo> updatePromiseInstallInfo(PackageInstallInfo installInfo) { 198 List<AppInfo> updatedAppInfos = new ArrayList<>(); 199 UserHandle user = installInfo.user; 200 for (int i = data.size() - 1; i >= 0; i--) { 201 final AppInfo appInfo = data.get(i); 202 final ComponentName tgtComp = appInfo.getTargetComponent(); 203 if (tgtComp != null && tgtComp.getPackageName().equals(installInfo.packageName) 204 && appInfo.user.equals(user)) { 205 if (installInfo.state == PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING 206 || installInfo.state == PackageInstallInfo.STATUS_INSTALLING 207 // In case unarchival fails, we would want to keep the icon and update 208 // back the progress to 0 for the all apps view without removing the 209 // icon, which is contrary to what happens during normal app installation 210 // flow. 211 || (installInfo.state == PackageInstallInfo.STATUS_FAILED 212 && appInfo.isArchived())) { 213 if (appInfo.isAppStartable() 214 && installInfo.state == PackageInstallInfo.STATUS_INSTALLING 215 && !appInfo.isArchived()) { 216 continue; 217 } 218 appInfo.setProgressLevel(installInfo); 219 220 updatedAppInfos.add(appInfo); 221 } else if (installInfo.state == PackageInstallInfo.STATUS_FAILED 222 && !appInfo.isAppStartable()) { 223 if (DEBUG) { 224 Log.w(TAG, "updatePromiseInstallInfo: removing app due to install" 225 + " failure and appInfo not startable." 226 + " package=" + appInfo.getTargetPackage()); 227 } 228 removeApp(i); 229 } 230 } 231 } 232 return updatedAppInfos; 233 } 234 removeApp(int index)235 private void removeApp(int index) { 236 AppInfo removed = data.remove(index); 237 if (removed != null) { 238 mDataChanged = true; 239 mRemoveListener.accept(removed); 240 } 241 } 242 clear()243 public void clear() { 244 data.clear(); 245 mDataChanged = false; 246 // Reset the index as locales might have changed 247 mIndex = new AlphabeticIndexCompat(LocaleList.getDefault()); 248 } 249 250 /** 251 * Add the icons for the supplied apk called packageName. 252 */ addPackage( Context context, String packageName, UserHandle user)253 public List<LauncherActivityInfo> addPackage( 254 Context context, String packageName, UserHandle user) { 255 List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class) 256 .getActivityList(packageName, user); 257 258 for (LauncherActivityInfo info : activities) { 259 add(new AppInfo(context, info, user), info); 260 } 261 262 return activities; 263 } 264 265 /** 266 * Remove the apps for the given apk identified by packageName. 267 */ removePackage(String packageName, UserHandle user)268 public void removePackage(String packageName, UserHandle user) { 269 final List<AppInfo> data = this.data; 270 for (int i = data.size() - 1; i >= 0; i--) { 271 AppInfo info = data.get(i); 272 if (info.user.equals(user) && packageName.equals(info.componentName.getPackageName())) { 273 removeApp(i); 274 } 275 } 276 } 277 278 /** 279 * Updates the disabled flags of apps matching {@param matcher} based on {@param op}. 280 */ updateDisabledFlags(Predicate<ItemInfo> matcher, FlagOp op)281 public void updateDisabledFlags(Predicate<ItemInfo> matcher, FlagOp op) { 282 final List<AppInfo> data = this.data; 283 for (int i = data.size() - 1; i >= 0; i--) { 284 AppInfo info = data.get(i); 285 if (matcher.test(info)) { 286 info.runtimeStatusFlags = op.apply(info.runtimeStatusFlags); 287 mDataChanged = true; 288 } 289 } 290 } 291 updateIconsAndLabels(HashSet<String> packages, UserHandle user)292 public void updateIconsAndLabels(HashSet<String> packages, UserHandle user) { 293 for (AppInfo info : data) { 294 if (info.user.equals(user) && packages.contains(info.componentName.getPackageName())) { 295 mIconCache.updateTitleAndIcon(info); 296 info.sectionName = mIndex.computeSectionName(info.title); 297 mDataChanged = true; 298 } 299 } 300 } 301 302 /** 303 * Add and remove icons for this package which has been updated. 304 */ updatePackage( Context context, String packageName, UserHandle user)305 public List<LauncherActivityInfo> updatePackage( 306 Context context, String packageName, UserHandle user) { 307 final ApiWrapper apiWrapper = ApiWrapper.INSTANCE.get(context); 308 final UserCache userCache = UserCache.getInstance(context); 309 final PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context); 310 final List<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class) 311 .getActivityList(packageName, user); 312 if (matches.size() > 0) { 313 // Find disabled/removed activities and remove them from data and add them 314 // to the removed list. 315 for (int i = data.size() - 1; i >= 0; i--) { 316 final AppInfo applicationInfo = data.get(i); 317 if (user.equals(applicationInfo.user) 318 && packageName.equals(applicationInfo.componentName.getPackageName())) { 319 if (!findActivity(matches, applicationInfo.componentName)) { 320 if (DEBUG) { 321 Log.w(TAG, "Changing shortcut target due to app component name change." 322 + " package=" + packageName); 323 } 324 removeApp(i); 325 } 326 } 327 } 328 329 // Find enabled activities and add them to the adapter 330 // Also updates existing activities with new labels/icons 331 for (final LauncherActivityInfo info : matches) { 332 AppInfo applicationInfo = findAppInfo(info.getComponentName(), user); 333 if (applicationInfo == null) { 334 add(new AppInfo(context, info, user), info); 335 } else { 336 Intent launchIntent = AppInfo.makeLaunchIntent(info); 337 338 mIconCache.getTitleAndIcon(applicationInfo, info, false /* useLowResIcon */); 339 applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title); 340 applicationInfo.intent = launchIntent; 341 AppInfo.updateRuntimeFlagsForActivityTarget(applicationInfo, info, 342 userCache.getUserInfo(user), apiWrapper, pmHelper); 343 mDataChanged = true; 344 } 345 } 346 } else { 347 // Remove all data for this package. 348 if (DEBUG) { 349 Log.w(TAG, "updatePromiseInstallInfo: no Activities matched updated package," 350 + " removing all apps from package=" + packageName); 351 } 352 for (int i = data.size() - 1; i >= 0; i--) { 353 final AppInfo applicationInfo = data.get(i); 354 if (user.equals(applicationInfo.user) 355 && packageName.equals(applicationInfo.componentName.getPackageName())) { 356 mIconCache.remove(applicationInfo.componentName, user); 357 removeApp(i); 358 } 359 } 360 } 361 362 return matches; 363 } 364 365 /** 366 * Returns whether <em>apps</em> contains <em>component</em>. 367 */ findActivity(List<LauncherActivityInfo> apps, ComponentName component)368 private static boolean findActivity(List<LauncherActivityInfo> apps, 369 ComponentName component) { 370 for (LauncherActivityInfo info : apps) { 371 if (info.getComponentName().equals(component)) { 372 return true; 373 } 374 } 375 return false; 376 } 377 378 /** 379 * Find an AppInfo object for the given componentName 380 * 381 * @return the corresponding AppInfo or null 382 */ findAppInfo(@onNull ComponentName componentName, @NonNull UserHandle user)383 public @Nullable AppInfo findAppInfo(@NonNull ComponentName componentName, 384 @NonNull UserHandle user) { 385 for (AppInfo info: data) { 386 if (componentName.equals(info.componentName) && user.equals(info.user)) { 387 return info; 388 } 389 } 390 return null; 391 } 392 copyData()393 public AppInfo[] copyData() { 394 AppInfo[] result = data.toArray(EMPTY_ARRAY); 395 Arrays.sort(result, COMPONENT_KEY_COMPARATOR); 396 return result; 397 } 398 trackRemoves(Consumer<AppInfo> removeListener)399 public SafeCloseable trackRemoves(Consumer<AppInfo> removeListener) { 400 mRemoveListener = removeListener; 401 402 return () -> mRemoveListener = NO_OP_CONSUMER; 403 } 404 } 405