• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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