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