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