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