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 android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID; 20 21 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 22 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; 23 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 24 import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent; 25 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; 26 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 27 28 import android.appwidget.AppWidgetManager; 29 import android.appwidget.AppWidgetProviderInfo; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.LauncherActivityInfo; 34 import android.content.pm.LauncherApps; 35 import android.content.pm.ShortcutInfo; 36 import android.os.UserHandle; 37 import android.util.Log; 38 import android.util.Pair; 39 40 import androidx.annotation.Nullable; 41 import androidx.annotation.WorkerThread; 42 43 import com.android.launcher3.Flags; 44 import com.android.launcher3.InvariantDeviceProfile; 45 import com.android.launcher3.Launcher; 46 import com.android.launcher3.LauncherAppState; 47 import com.android.launcher3.LauncherSettings.Favorites; 48 import com.android.launcher3.logging.FileLog; 49 import com.android.launcher3.model.data.ItemInfo; 50 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 51 import com.android.launcher3.model.data.WorkspaceItemInfo; 52 import com.android.launcher3.shortcuts.ShortcutKey; 53 import com.android.launcher3.shortcuts.ShortcutRequest; 54 import com.android.launcher3.util.MainThreadInitializedObject; 55 import com.android.launcher3.util.PersistedItemArray; 56 import com.android.launcher3.util.Preconditions; 57 import com.android.launcher3.util.SafeCloseable; 58 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 59 60 import java.util.HashSet; 61 import java.util.List; 62 import java.util.stream.Collectors; 63 import java.util.stream.Stream; 64 65 /** 66 * Class to maintain a queue of pending items to be added to the workspace. 67 */ 68 public class ItemInstallQueue implements SafeCloseable { 69 70 private static final String LOG = "ItemInstallQueue"; 71 72 public static final int FLAG_ACTIVITY_PAUSED = 1; 73 public static final int FLAG_LOADER_RUNNING = 2; 74 public static final int FLAG_DRAG_AND_DROP = 4; 75 76 private static final String TAG = "InstallShortcutReceiver"; 77 78 // The set of shortcuts that are pending install 79 private static final String APPS_PENDING_INSTALL = "apps_to_install"; 80 81 public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; 82 public static final int NEW_SHORTCUT_STAGGER_DELAY = 85; 83 84 public static MainThreadInitializedObject<ItemInstallQueue> INSTANCE = 85 new MainThreadInitializedObject<>(ItemInstallQueue::new); 86 87 private final PersistedItemArray<PendingInstallShortcutInfo> mStorage = 88 new PersistedItemArray<>(APPS_PENDING_INSTALL); 89 private final Context mContext; 90 91 // Determines whether to defer installing shortcuts immediately until 92 // processAllPendingInstalls() is called. 93 private int mInstallQueueDisabledFlags = 0; 94 95 // Only accessed on worker thread 96 private List<PendingInstallShortcutInfo> mItems; 97 ItemInstallQueue(Context context)98 private ItemInstallQueue(Context context) { 99 mContext = context; 100 } 101 102 @Override close()103 public void close() {} 104 105 @WorkerThread ensureQueueLoaded()106 private void ensureQueueLoaded() { 107 Preconditions.assertWorkerThread(); 108 if (mItems == null) { 109 mItems = mStorage.read(mContext, this::decode); 110 } 111 } 112 113 @WorkerThread addToQueue(PendingInstallShortcutInfo info)114 private void addToQueue(PendingInstallShortcutInfo info) { 115 ensureQueueLoaded(); 116 if (!mItems.contains(info)) { 117 mItems.add(info); 118 mStorage.write(mContext, mItems); 119 } 120 } 121 122 @WorkerThread flushQueueInBackground()123 private void flushQueueInBackground() { 124 Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity(); 125 if (launcher == null) { 126 // Launcher not loaded 127 return; 128 } 129 ensureQueueLoaded(); 130 if (mItems.isEmpty()) { 131 return; 132 } 133 134 List<Pair<ItemInfo, Object>> installQueue = mItems.stream() 135 .map(info -> info.getItemInfo(mContext)) 136 .collect(Collectors.toList()); 137 138 // Add the items and clear queue 139 if (!installQueue.isEmpty()) { 140 // add log 141 launcher.getModel().addAndBindAddedWorkspaceItems(installQueue); 142 } 143 mItems.clear(); 144 mStorage.getFile(mContext).delete(); 145 } 146 147 /** 148 * Removes previously added items from the queue. 149 */ 150 @WorkerThread removeFromInstallQueue(HashSet<String> packageNames, UserHandle user)151 public void removeFromInstallQueue(HashSet<String> packageNames, UserHandle user) { 152 if (packageNames.isEmpty()) { 153 return; 154 } 155 ensureQueueLoaded(); 156 if (mItems.removeIf(item -> 157 item.user.equals(user) && packageNames.contains(getIntentPackage(item.intent)))) { 158 mStorage.write(mContext, mItems); 159 } 160 } 161 162 /** 163 * Adds an item to the install queue 164 */ queueItem(ShortcutInfo info)165 public void queueItem(ShortcutInfo info) { 166 queuePendingShortcutInfo(new PendingInstallShortcutInfo(info)); 167 } 168 169 /** 170 * Adds an item to the install queue 171 */ queueItem(AppWidgetProviderInfo info, int widgetId)172 public void queueItem(AppWidgetProviderInfo info, int widgetId) { 173 queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, widgetId)); 174 } 175 176 /** 177 * Adds an item to the install queue 178 */ queueItem(String packageName, UserHandle userHandle)179 public void queueItem(String packageName, UserHandle userHandle) { 180 queuePendingShortcutInfo(new PendingInstallShortcutInfo(packageName, userHandle)); 181 } 182 183 /** 184 * Returns a stream of all pending shortcuts in the queue 185 */ 186 @WorkerThread getPendingShortcuts(UserHandle user)187 public Stream<ShortcutKey> getPendingShortcuts(UserHandle user) { 188 ensureQueueLoaded(); 189 return mItems.stream() 190 .filter(item -> item.itemType == ITEM_TYPE_DEEP_SHORTCUT && user.equals(item.user)) 191 .map(item -> ShortcutKey.fromIntent(item.intent, user)); 192 } 193 queuePendingShortcutInfo(PendingInstallShortcutInfo info)194 private void queuePendingShortcutInfo(PendingInstallShortcutInfo info) { 195 final Exception stackTrace = new Exception(); 196 197 // Queue the item up for adding if launcher has not loaded properly yet 198 MODEL_EXECUTOR.post(() -> { 199 Pair<ItemInfo, Object> itemInfo = info.getItemInfo(mContext); 200 if (itemInfo == null) { 201 FileLog.d(LOG, 202 "Adding PendingInstallShortcutInfo with no attached info to queue.", 203 stackTrace); 204 } else { 205 FileLog.d(LOG, 206 "Adding PendingInstallShortcutInfo to queue. Attached info: " 207 + itemInfo.first, 208 stackTrace); 209 } 210 211 addToQueue(info); 212 }); 213 flushInstallQueue(); 214 } 215 216 /** 217 * Pauses the push-to-model flow until unpaused. All items are held in the queue and 218 * not added to the model. 219 */ pauseModelPush(int flag)220 public void pauseModelPush(int flag) { 221 mInstallQueueDisabledFlags |= flag; 222 } 223 224 /** 225 * Adds all the queue items to the model if the use is completely resumed. 226 */ resumeModelPush(int flag)227 public void resumeModelPush(int flag) { 228 mInstallQueueDisabledFlags &= ~flag; 229 flushInstallQueue(); 230 } 231 flushInstallQueue()232 private void flushInstallQueue() { 233 if (mInstallQueueDisabledFlags != 0) { 234 return; 235 } 236 MODEL_EXECUTOR.post(this::flushQueueInBackground); 237 } 238 239 private static class PendingInstallShortcutInfo extends ItemInfo { 240 241 final Intent intent; 242 243 @Nullable ShortcutInfo shortcutInfo; 244 @Nullable AppWidgetProviderInfo providerInfo; 245 246 /** 247 * Initializes a PendingInstallShortcutInfo to represent a pending launcher target. 248 */ PendingInstallShortcutInfo(String packageName, UserHandle userHandle)249 public PendingInstallShortcutInfo(String packageName, UserHandle userHandle) { 250 itemType = Favorites.ITEM_TYPE_APPLICATION; 251 intent = new Intent().setPackage(packageName); 252 user = userHandle; 253 } 254 255 /** 256 * Initializes a PendingInstallShortcutInfo to represent a deep shortcut. 257 */ PendingInstallShortcutInfo(ShortcutInfo info)258 public PendingInstallShortcutInfo(ShortcutInfo info) { 259 itemType = Favorites.ITEM_TYPE_DEEP_SHORTCUT; 260 intent = ShortcutKey.makeIntent(info); 261 user = info.getUserHandle(); 262 263 shortcutInfo = info; 264 } 265 266 /** 267 * Initializes a PendingInstallShortcutInfo to represent an app widget. 268 */ PendingInstallShortcutInfo(AppWidgetProviderInfo info, int widgetId)269 public PendingInstallShortcutInfo(AppWidgetProviderInfo info, int widgetId) { 270 itemType = Favorites.ITEM_TYPE_APPWIDGET; 271 intent = new Intent() 272 .setComponent(info.provider) 273 .putExtra(EXTRA_APPWIDGET_ID, widgetId); 274 user = info.getProfile(); 275 276 providerInfo = info; 277 } 278 279 @Override 280 @Nullable getIntent()281 public Intent getIntent() { 282 return intent; 283 } 284 285 @SuppressWarnings("NewApi") getItemInfo(Context context)286 public Pair<ItemInfo, Object> getItemInfo(Context context) { 287 switch (itemType) { 288 case ITEM_TYPE_APPLICATION: { 289 String packageName = intent.getPackage(); 290 List<LauncherActivityInfo> laiList = 291 context.getSystemService(LauncherApps.class) 292 .getActivityList(packageName, user); 293 294 final WorkspaceItemInfo si = new WorkspaceItemInfo(); 295 si.user = user; 296 297 LauncherActivityInfo lai; 298 boolean usePackageIcon = laiList.isEmpty(); 299 if (usePackageIcon) { 300 lai = null; 301 si.intent = makeLaunchIntent(new ComponentName(packageName, "")) 302 .setPackage(packageName); 303 si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; 304 } else { 305 lai = laiList.get(0); 306 si.intent = makeLaunchIntent(lai); 307 if (Flags.enableSupportForArchiving() 308 && lai.getActivityInfo().isArchived) { 309 si.runtimeStatusFlags |= FLAG_ARCHIVED; 310 } 311 } 312 LauncherAppState.getInstance(context).getIconCache() 313 .getTitleAndIcon(si, () -> lai, usePackageIcon, false); 314 return Pair.create(si, null); 315 } 316 case ITEM_TYPE_DEEP_SHORTCUT: { 317 WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, context); 318 LauncherAppState.getInstance(context).getIconCache() 319 .getShortcutIcon(itemInfo, shortcutInfo); 320 return Pair.create(itemInfo, shortcutInfo); 321 } 322 case ITEM_TYPE_APPWIDGET: { 323 LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo 324 .fromProviderInfo(context, providerInfo); 325 LauncherAppWidgetInfo widgetInfo = new LauncherAppWidgetInfo( 326 intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0), 327 info.provider); 328 InvariantDeviceProfile idp = LauncherAppState.getIDP(context); 329 widgetInfo.minSpanX = info.minSpanX; 330 widgetInfo.minSpanY = info.minSpanY; 331 widgetInfo.spanX = Math.min(info.spanX, idp.numColumns); 332 widgetInfo.spanY = Math.min(info.spanY, idp.numRows); 333 widgetInfo.user = user; 334 return Pair.create(widgetInfo, providerInfo); 335 } 336 } 337 return null; 338 } 339 340 @Override equals(Object obj)341 public boolean equals(Object obj) { 342 if (obj instanceof PendingInstallShortcutInfo) { 343 PendingInstallShortcutInfo other = (PendingInstallShortcutInfo) obj; 344 345 boolean userMatches = user.equals(other.user); 346 boolean itemTypeMatches = itemType == other.itemType; 347 boolean intentMatches = intent.toUri(0).equals(other.intent.toUri(0)); 348 boolean shortcutInfoMatches = shortcutInfo == null 349 ? other.shortcutInfo == null 350 : other.shortcutInfo != null 351 && shortcutInfo.getId().equals(other.shortcutInfo.getId()) 352 && shortcutInfo.getPackage().equals(other.shortcutInfo.getPackage()); 353 boolean providerInfoMatches = providerInfo == null 354 ? other.providerInfo == null 355 : other.providerInfo != null 356 && providerInfo.provider.equals(other.providerInfo.provider); 357 358 return userMatches 359 && itemTypeMatches 360 && intentMatches 361 && shortcutInfoMatches 362 && providerInfoMatches; 363 } 364 return false; 365 } 366 } 367 getIntentPackage(Intent intent)368 private static String getIntentPackage(Intent intent) { 369 return intent.getComponent() == null 370 ? intent.getPackage() : intent.getComponent().getPackageName(); 371 } 372 decode(int itemType, UserHandle user, Intent intent)373 private PendingInstallShortcutInfo decode(int itemType, UserHandle user, Intent intent) { 374 switch (itemType) { 375 case Favorites.ITEM_TYPE_APPLICATION: 376 return new PendingInstallShortcutInfo(intent.getPackage(), user); 377 case Favorites.ITEM_TYPE_DEEP_SHORTCUT: { 378 List<ShortcutInfo> si = ShortcutKey.fromIntent(intent, user) 379 .buildRequest(mContext) 380 .query(ShortcutRequest.ALL); 381 if (si.isEmpty()) { 382 return null; 383 } else { 384 return new PendingInstallShortcutInfo(si.get(0)); 385 } 386 } 387 case Favorites.ITEM_TYPE_APPWIDGET: { 388 int widgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0); 389 AppWidgetProviderInfo info = 390 AppWidgetManager.getInstance(mContext).getAppWidgetInfo(widgetId); 391 if (info == null || !info.provider.equals(intent.getComponent()) 392 || !info.getProfile().equals(user)) { 393 return null; 394 } 395 return new PendingInstallShortcutInfo(info, widgetId); 396 } 397 default: 398 Log.e(TAG, "Unknown item type"); 399 } 400 return null; 401 } 402 } 403