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