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.DESKTOP_ICON_FLAG; 22 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 23 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET; 24 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 25 import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent; 26 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; 27 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 28 29 import android.appwidget.AppWidgetManager; 30 import android.appwidget.AppWidgetProviderInfo; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.LauncherActivityInfo; 35 import android.content.pm.LauncherApps; 36 import android.content.pm.ShortcutInfo; 37 import android.os.UserHandle; 38 import android.util.Log; 39 import android.util.Pair; 40 41 import androidx.annotation.Nullable; 42 import androidx.annotation.WorkerThread; 43 44 import com.android.launcher3.Flags; 45 import com.android.launcher3.InvariantDeviceProfile; 46 import com.android.launcher3.Launcher; 47 import com.android.launcher3.LauncherAppState; 48 import com.android.launcher3.LauncherSettings.Favorites; 49 import com.android.launcher3.dagger.ApplicationContext; 50 import com.android.launcher3.dagger.LauncherAppSingleton; 51 import com.android.launcher3.dagger.LauncherBaseAppComponent; 52 import com.android.launcher3.logging.FileLog; 53 import com.android.launcher3.model.data.ItemInfo; 54 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 55 import com.android.launcher3.model.data.WorkspaceItemInfo; 56 import com.android.launcher3.shortcuts.ShortcutKey; 57 import com.android.launcher3.shortcuts.ShortcutRequest; 58 import com.android.launcher3.util.DaggerSingletonObject; 59 import com.android.launcher3.util.PersistedItemArray; 60 import com.android.launcher3.util.Preconditions; 61 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 62 63 import java.util.HashSet; 64 import java.util.List; 65 import java.util.stream.Collectors; 66 import java.util.stream.Stream; 67 68 import javax.inject.Inject; 69 70 /** 71 * Class to maintain a queue of pending items to be added to the workspace. 72 */ 73 @LauncherAppSingleton 74 public class ItemInstallQueue { 75 76 private static final String LOG = "ItemInstallQueue"; 77 78 public static final int FLAG_ACTIVITY_PAUSED = 1; 79 public static final int FLAG_LOADER_RUNNING = 2; 80 public static final int FLAG_DRAG_AND_DROP = 4; 81 82 private static final String TAG = "InstallShortcutReceiver"; 83 84 // The set of shortcuts that are pending install 85 private static final String APPS_PENDING_INSTALL = "apps_to_install"; 86 87 public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450; 88 public static final int NEW_SHORTCUT_STAGGER_DELAY = 85; 89 90 public static DaggerSingletonObject<ItemInstallQueue> INSTANCE = 91 new DaggerSingletonObject<>(LauncherBaseAppComponent::getItemInstallQueue); 92 private final PersistedItemArray<PendingInstallShortcutInfo> mStorage = 93 new PersistedItemArray<>(APPS_PENDING_INSTALL); 94 private final Context mContext; 95 96 // Determines whether to defer installing shortcuts immediately until 97 // processAllPendingInstalls() is called. 98 private int mInstallQueueDisabledFlags = 0; 99 100 // Only accessed on worker thread 101 private List<PendingInstallShortcutInfo> mItems; 102 103 @Inject ItemInstallQueue(@pplicationContext Context context)104 public ItemInstallQueue(@ApplicationContext Context context) { 105 mContext = context; 106 } 107 108 @WorkerThread ensureQueueLoaded()109 private void ensureQueueLoaded() { 110 Preconditions.assertWorkerThread(); 111 if (mItems == null) { 112 mItems = mStorage.read(mContext, this::decode); 113 } 114 } 115 116 @WorkerThread addToQueue(PendingInstallShortcutInfo info)117 private void addToQueue(PendingInstallShortcutInfo info) { 118 ensureQueueLoaded(); 119 if (!mItems.contains(info)) { 120 mItems.add(info); 121 mStorage.write(mContext, mItems); 122 } 123 } 124 125 @WorkerThread flushQueueInBackground()126 private void flushQueueInBackground() { 127 Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedContext(); 128 if (launcher == null) { 129 // Launcher not loaded 130 return; 131 } 132 ensureQueueLoaded(); 133 if (mItems.isEmpty()) { 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 // add log 144 launcher.getModel().addAndBindAddedWorkspaceItems(installQueue); 145 } 146 mItems.clear(); 147 mStorage.getFile(mContext).delete(); 148 } 149 150 /** 151 * Removes previously added items from the queue. 152 */ 153 @WorkerThread removeFromInstallQueue(HashSet<String> packageNames, UserHandle user)154 public void removeFromInstallQueue(HashSet<String> packageNames, UserHandle user) { 155 if (packageNames.isEmpty()) { 156 return; 157 } 158 ensureQueueLoaded(); 159 if (mItems.removeIf(item -> 160 item.user.equals(user) && packageNames.contains(getIntentPackage(item.intent)))) { 161 mStorage.write(mContext, mItems); 162 } 163 } 164 165 /** 166 * Adds an item to the install queue 167 */ queueItem(ShortcutInfo info)168 public void queueItem(ShortcutInfo info) { 169 queuePendingShortcutInfo(new PendingInstallShortcutInfo(info)); 170 } 171 172 /** 173 * Adds an item to the install queue 174 */ queueItem(AppWidgetProviderInfo info, int widgetId)175 public void queueItem(AppWidgetProviderInfo info, int widgetId) { 176 queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, widgetId)); 177 } 178 179 /** 180 * Adds an item to the install queue 181 */ queueItem(String packageName, UserHandle userHandle)182 public void queueItem(String packageName, UserHandle userHandle) { 183 queuePendingShortcutInfo(new PendingInstallShortcutInfo(packageName, userHandle)); 184 } 185 186 /** 187 * Returns a stream of all pending shortcuts in the queue 188 */ 189 @WorkerThread getPendingShortcuts(UserHandle user)190 public Stream<ShortcutKey> getPendingShortcuts(UserHandle user) { 191 ensureQueueLoaded(); 192 return mItems.stream() 193 .filter(item -> item.itemType == ITEM_TYPE_DEEP_SHORTCUT && user.equals(item.user)) 194 .map(item -> ShortcutKey.fromIntent(item.intent, user)); 195 } 196 queuePendingShortcutInfo(PendingInstallShortcutInfo info)197 private void queuePendingShortcutInfo(PendingInstallShortcutInfo info) { 198 199 // Queue the item up for adding if launcher has not loaded properly yet 200 MODEL_EXECUTOR.post(() -> { 201 Pair<ItemInfo, Object> itemInfo = info.getItemInfo(mContext); 202 if (itemInfo == null) { 203 FileLog.d(LOG, 204 "Adding PendingInstallShortcutInfo with no attached info to queue."); 205 } else { 206 FileLog.d(LOG, 207 "Adding PendingInstallShortcutInfo to queue." 208 + " Attached info: " + itemInfo.first); 209 } 210 addToQueue(info); 211 }); 212 flushInstallQueue(); 213 } 214 215 /** 216 * Pauses the push-to-model flow until unpaused. All items are held in the queue and 217 * not added to the model. 218 */ pauseModelPush(int flag)219 public void pauseModelPush(int flag) { 220 mInstallQueueDisabledFlags |= flag; 221 } 222 223 /** 224 * Adds all the queue items to the model if the use is completely resumed. 225 */ resumeModelPush(int flag)226 public void resumeModelPush(int flag) { 227 mInstallQueueDisabledFlags &= ~flag; 228 flushInstallQueue(); 229 } 230 flushInstallQueue()231 private void flushInstallQueue() { 232 if (mInstallQueueDisabledFlags != 0) { 233 return; 234 } 235 MODEL_EXECUTOR.post(this::flushQueueInBackground); 236 } 237 238 private static class PendingInstallShortcutInfo extends ItemInfo { 239 240 final Intent intent; 241 242 @Nullable ShortcutInfo shortcutInfo; 243 @Nullable AppWidgetProviderInfo providerInfo; 244 245 /** 246 * Initializes a PendingInstallShortcutInfo to represent a pending launcher target. 247 */ PendingInstallShortcutInfo(String packageName, UserHandle userHandle)248 public PendingInstallShortcutInfo(String packageName, UserHandle userHandle) { 249 itemType = Favorites.ITEM_TYPE_APPLICATION; 250 intent = new Intent().setPackage(packageName); 251 user = userHandle; 252 } 253 254 /** 255 * Initializes a PendingInstallShortcutInfo to represent a deep shortcut. 256 */ PendingInstallShortcutInfo(ShortcutInfo info)257 public PendingInstallShortcutInfo(ShortcutInfo info) { 258 itemType = Favorites.ITEM_TYPE_DEEP_SHORTCUT; 259 intent = ShortcutKey.makeIntent(info); 260 user = info.getUserHandle(); 261 262 shortcutInfo = info; 263 } 264 265 /** 266 * Initializes a PendingInstallShortcutInfo to represent an app widget. 267 */ PendingInstallShortcutInfo(AppWidgetProviderInfo info, int widgetId)268 public PendingInstallShortcutInfo(AppWidgetProviderInfo info, int widgetId) { 269 itemType = Favorites.ITEM_TYPE_APPWIDGET; 270 intent = new Intent() 271 .setComponent(info.provider) 272 .putExtra(EXTRA_APPWIDGET_ID, widgetId); 273 user = info.getProfile(); 274 275 providerInfo = info; 276 } 277 278 @Override 279 @Nullable getIntent()280 public Intent getIntent() { 281 return intent; 282 } 283 284 @SuppressWarnings("NewApi") getItemInfo(Context context)285 public Pair<ItemInfo, Object> getItemInfo(Context context) { 286 switch (itemType) { 287 case ITEM_TYPE_APPLICATION: { 288 String packageName = intent.getPackage(); 289 List<LauncherActivityInfo> laiList = 290 context.getSystemService(LauncherApps.class) 291 .getActivityList(packageName, user); 292 293 final WorkspaceItemInfo si = new WorkspaceItemInfo(); 294 si.user = user; 295 296 LauncherActivityInfo lai; 297 boolean usePackageIcon = laiList.isEmpty(); 298 if (usePackageIcon) { 299 lai = null; 300 si.intent = makeLaunchIntent(new ComponentName(packageName, "")) 301 .setPackage(packageName); 302 si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON; 303 } else { 304 lai = laiList.get(0); 305 si.intent = makeLaunchIntent(lai); 306 if (Flags.enableSupportForArchiving() 307 && lai.getActivityInfo().isArchived) { 308 si.runtimeStatusFlags |= FLAG_ARCHIVED; 309 } 310 } 311 LauncherAppState.getInstance(context).getIconCache() 312 .getTitleAndIcon(si, () -> lai, 313 DESKTOP_ICON_FLAG.withUsePackageIcon(usePackageIcon)); 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