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