1 /*
2  * Copyright (C) 2016 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 package com.android.launcher3.model;
17 
18 import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
19 
20 import android.content.Intent;
21 import android.content.pm.LauncherActivityInfo;
22 import android.content.pm.LauncherApps;
23 import android.content.pm.PackageInstaller.SessionInfo;
24 import android.os.UserHandle;
25 import android.util.LongSparseArray;
26 import android.util.Pair;
27 
28 import com.android.launcher3.InvariantDeviceProfile;
29 import com.android.launcher3.LauncherAppState;
30 import com.android.launcher3.LauncherModel.CallbackTask;
31 import com.android.launcher3.LauncherSettings;
32 import com.android.launcher3.config.FeatureFlags;
33 import com.android.launcher3.logging.FileLog;
34 import com.android.launcher3.model.BgDataModel.Callbacks;
35 import com.android.launcher3.model.data.AppInfo;
36 import com.android.launcher3.model.data.FolderInfo;
37 import com.android.launcher3.model.data.ItemInfo;
38 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
39 import com.android.launcher3.model.data.WorkspaceItemInfo;
40 import com.android.launcher3.pm.InstallSessionHelper;
41 import com.android.launcher3.pm.PackageInstallInfo;
42 import com.android.launcher3.util.GridOccupancy;
43 import com.android.launcher3.util.IntArray;
44 import com.android.launcher3.util.IntSet;
45 import com.android.launcher3.util.PackageManagerHelper;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * Task to add auto-created workspace items.
52  */
53 public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
54 
55     private static final String LOG = "AddWorkspaceItemsTask";
56 
57     private final List<Pair<ItemInfo, Object>> mItemList;
58 
59     /**
60      * @param itemList items to add on the workspace
61      */
AddWorkspaceItemsTask(List<Pair<ItemInfo, Object>> itemList)62     public AddWorkspaceItemsTask(List<Pair<ItemInfo, Object>> itemList) {
63         mItemList = itemList;
64     }
65 
66     @Override
execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps)67     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
68         if (mItemList.isEmpty()) {
69             return;
70         }
71 
72         final ArrayList<ItemInfo> addedItemsFinal = new ArrayList<>();
73         final IntArray addedWorkspaceScreensFinal = new IntArray();
74 
75         synchronized(dataModel) {
76             IntArray workspaceScreens = dataModel.collectWorkspaceScreens();
77 
78             List<ItemInfo> filteredItems = new ArrayList<>();
79             for (Pair<ItemInfo, Object> entry : mItemList) {
80                 ItemInfo item = entry.first;
81                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
82                         item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
83                     // Short-circuit this logic if the icon exists somewhere on the workspace
84                     if (shortcutExists(dataModel, item.getIntent(), item.user)) {
85                         continue;
86                     }
87 
88                     // b/139663018 Short-circuit this logic if the icon is a system app
89                     if (PackageManagerHelper.isSystemApp(app.getContext(), item.getIntent())) {
90                         continue;
91                     }
92                 }
93 
94                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
95                     if (item instanceof AppInfo) {
96                         item = ((AppInfo) item).makeWorkspaceItem();
97                     }
98                 }
99                 if (item != null) {
100                     filteredItems.add(item);
101                 }
102             }
103 
104             InstallSessionHelper packageInstaller =
105                     InstallSessionHelper.INSTANCE.get(app.getContext());
106             LauncherApps launcherApps = app.getContext().getSystemService(LauncherApps.class);
107 
108             for (ItemInfo item : filteredItems) {
109                 // Find appropriate space for the item.
110                 int[] coords = findSpaceForItem(app, dataModel, workspaceScreens,
111                         addedWorkspaceScreensFinal, item.spanX, item.spanY);
112                 int screenId = coords[0];
113 
114                 ItemInfo itemInfo;
115                 if (item instanceof WorkspaceItemInfo || item instanceof FolderInfo ||
116                         item instanceof LauncherAppWidgetInfo) {
117                     itemInfo = item;
118                 } else if (item instanceof AppInfo) {
119                     itemInfo = ((AppInfo) item).makeWorkspaceItem();
120                 } else {
121                     throw new RuntimeException("Unexpected info type");
122                 }
123 
124                 if (item instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) item).isPromise()) {
125                     WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) item;
126                     String packageName = item.getTargetComponent() != null
127                             ? item.getTargetComponent().getPackageName() : null;
128                     if (packageName == null) {
129                         continue;
130                     }
131                     SessionInfo sessionInfo = packageInstaller.getActiveSessionInfo(item.user,
132                             packageName);
133 
134                     if (!packageInstaller.verifySessionInfo(sessionInfo)) {
135                         FileLog.d(LOG, "Item info failed session info verification. "
136                                 + "Skipping : " + workspaceInfo);
137                         continue;
138                     }
139 
140                     List<LauncherActivityInfo> activities = launcherApps
141                             .getActivityList(packageName, item.user);
142                     boolean hasActivity = activities != null && !activities.isEmpty();
143 
144                     if (sessionInfo == null) {
145                         if (!hasActivity) {
146                             // Session was cancelled, do not add.
147                             continue;
148                         }
149                     } else {
150                         workspaceInfo.setProgressLevel(
151                                 (int) (sessionInfo.getProgress() * 100),
152                                 PackageInstallInfo.STATUS_INSTALLING);
153                     }
154 
155                     if (hasActivity) {
156                         // App was installed while launcher was in the background,
157                         // or app was already installed for another user.
158                         itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user)
159                                 .makeWorkspaceItem();
160 
161                         if (shortcutExists(dataModel, itemInfo.getIntent(), itemInfo.user)) {
162                             // We need this additional check here since we treat all auto added
163                             // workspace items as promise icons. At this point we now have the
164                             // correct intent to compare against existing workspace icons.
165                             // Icon already exists on the workspace and should not be auto-added.
166                             continue;
167                         }
168 
169                         WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
170                         wii.title = "";
171                         wii.bitmap = app.getIconCache().getDefaultIcon(item.user);
172                         app.getIconCache().getTitleAndIcon(wii,
173                                 ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
174                     }
175                 }
176 
177                 // Add the shortcut to the db
178                 getModelWriter().addItemToDatabase(itemInfo,
179                         LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId,
180                         coords[1], coords[2]);
181 
182                 // Save the WorkspaceItemInfo for binding in the workspace
183                 addedItemsFinal.add(itemInfo);
184 
185                 // log bitmap and label
186                 FileLog.d(LOG, "Adding item info to workspace: " + itemInfo);
187             }
188         }
189 
190         if (!addedItemsFinal.isEmpty()) {
191             scheduleCallbackTask(new CallbackTask() {
192                 @Override
193                 public void execute(Callbacks callbacks) {
194                     final ArrayList<ItemInfo> addAnimated = new ArrayList<>();
195                     final ArrayList<ItemInfo> addNotAnimated = new ArrayList<>();
196                     if (!addedItemsFinal.isEmpty()) {
197                         ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1);
198                         int lastScreenId = info.screenId;
199                         for (ItemInfo i : addedItemsFinal) {
200                             if (i.screenId == lastScreenId) {
201                                 addAnimated.add(i);
202                             } else {
203                                 addNotAnimated.add(i);
204                             }
205                         }
206                     }
207                     callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
208                             addNotAnimated, addAnimated);
209                 }
210             });
211         }
212     }
213 
214     /**
215      * Returns true if the shortcuts already exists on the workspace. This must be called after
216      * the workspace has been loaded. We identify a shortcut by its intent.
217      */
shortcutExists(BgDataModel dataModel, Intent intent, UserHandle user)218     protected boolean shortcutExists(BgDataModel dataModel, Intent intent, UserHandle user) {
219         final String compPkgName, intentWithPkg, intentWithoutPkg;
220         if (intent == null) {
221             // Skip items with null intents
222             return true;
223         }
224         if (intent.getComponent() != null) {
225             // If component is not null, an intent with null package will produce
226             // the same result and should also be a match.
227             compPkgName = intent.getComponent().getPackageName();
228             if (intent.getPackage() != null) {
229                 intentWithPkg = intent.toUri(0);
230                 intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0);
231             } else {
232                 intentWithPkg = new Intent(intent).setPackage(compPkgName).toUri(0);
233                 intentWithoutPkg = intent.toUri(0);
234             }
235         } else {
236             compPkgName = null;
237             intentWithPkg = intent.toUri(0);
238             intentWithoutPkg = intent.toUri(0);
239         }
240 
241         boolean isLauncherAppTarget = PackageManagerHelper.isLauncherAppTarget(intent);
242         synchronized (dataModel) {
243             for (ItemInfo item : dataModel.itemsIdMap) {
244                 if (item instanceof WorkspaceItemInfo) {
245                     WorkspaceItemInfo info = (WorkspaceItemInfo) item;
246                     if (item.getIntent() != null && info.user.equals(user)) {
247                         Intent copyIntent = new Intent(item.getIntent());
248                         copyIntent.setSourceBounds(intent.getSourceBounds());
249                         String s = copyIntent.toUri(0);
250                         if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) {
251                             return true;
252                         }
253 
254                         // checking for existing promise icon with same package name
255                         if (isLauncherAppTarget
256                                 && info.isPromise()
257                                 && info.hasStatusFlag(WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON)
258                                 && info.getTargetComponent() != null
259                                 && compPkgName != null
260                                 && compPkgName.equals(info.getTargetComponent().getPackageName())) {
261                             return true;
262                         }
263                     }
264                 }
265             }
266         }
267         return false;
268     }
269 
270     /**
271      * Find a position on the screen for the given size or adds a new screen.
272      * @return screenId and the coordinates for the item in an int array of size 3.
273      */
findSpaceForItem( LauncherAppState app, BgDataModel dataModel, IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY)274     protected int[] findSpaceForItem( LauncherAppState app, BgDataModel dataModel,
275             IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) {
276         LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>();
277 
278         // Use sBgItemsIdMap as all the items are already loaded.
279         synchronized (dataModel) {
280             for (ItemInfo info : dataModel.itemsIdMap) {
281                 if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
282                     ArrayList<ItemInfo> items = screenItems.get(info.screenId);
283                     if (items == null) {
284                         items = new ArrayList<>();
285                         screenItems.put(info.screenId, items);
286                     }
287                     items.add(info);
288                 }
289             }
290         }
291 
292         // Find appropriate space for the item.
293         int screenId = 0;
294         int[] coordinates = new int[2];
295         boolean found = false;
296 
297         int screenCount = workspaceScreens.size();
298         // First check the preferred screen.
299         IntSet screensToExclude = new IntSet();
300         if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
301             screensToExclude.add(FIRST_SCREEN_ID);
302         }
303 
304         for (int screen = 0; screen < screenCount; screen++) {
305             screenId = workspaceScreens.get(screen);
306             if (!screensToExclude.contains(screenId) && findNextAvailableIconSpaceInScreen(
307                     app, screenItems.get(screenId), coordinates, spanX, spanY)) {
308                 // We found a space for it
309                 found = true;
310                 break;
311             }
312         }
313 
314         if (!found) {
315             // Still no position found. Add a new screen to the end.
316             screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(),
317                     LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
318                     .getInt(LauncherSettings.Settings.EXTRA_VALUE);
319 
320             // Save the screen id for binding in the workspace
321             workspaceScreens.add(screenId);
322             addedWorkspaceScreensFinal.add(screenId);
323 
324             // If we still can't find an empty space, then God help us all!!!
325             if (!findNextAvailableIconSpaceInScreen(
326                     app, screenItems.get(screenId), coordinates, spanX, spanY)) {
327                 throw new RuntimeException("Can't find space to add the item");
328             }
329         }
330         return new int[] {screenId, coordinates[0], coordinates[1]};
331     }
332 
findNextAvailableIconSpaceInScreen( LauncherAppState app, ArrayList<ItemInfo> occupiedPos, int[] xy, int spanX, int spanY)333     private boolean findNextAvailableIconSpaceInScreen(
334             LauncherAppState app, ArrayList<ItemInfo> occupiedPos,
335             int[] xy, int spanX, int spanY) {
336         InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
337 
338         GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows);
339         if (occupiedPos != null) {
340             for (ItemInfo r : occupiedPos) {
341                 occupied.markCells(r, true);
342             }
343         }
344         return occupied.findVacantCell(xy, spanX, spanY);
345     }
346 
347 }
348