• 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.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