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 */ 16package com.android.launcher3.model; 17 18import android.content.Context; 19import android.content.Intent; 20import android.content.pm.LauncherActivityInfo; 21import android.os.Process; 22import android.os.UserHandle; 23import android.util.ArrayMap; 24import android.util.LongSparseArray; 25import android.util.Pair; 26import com.android.launcher3.AllAppsList; 27import com.android.launcher3.AppInfo; 28import com.android.launcher3.FolderInfo; 29import com.android.launcher3.InvariantDeviceProfile; 30import com.android.launcher3.ItemInfo; 31import com.android.launcher3.LauncherAppState; 32import com.android.launcher3.LauncherAppWidgetInfo; 33import com.android.launcher3.LauncherModel; 34import com.android.launcher3.LauncherModel.CallbackTask; 35import com.android.launcher3.LauncherModel.Callbacks; 36import com.android.launcher3.LauncherSettings; 37import com.android.launcher3.ShortcutInfo; 38import com.android.launcher3.Utilities; 39import com.android.launcher3.util.GridOccupancy; 40import com.android.launcher3.util.ManagedProfileHeuristic.UserFolderInfo; 41import com.android.launcher3.util.Provider; 42import java.util.ArrayList; 43import java.util.List; 44 45/** 46 * Task to add auto-created workspace items. 47 */ 48public class AddWorkspaceItemsTask extends BaseModelUpdateTask { 49 50 private final Provider<List<Pair<ItemInfo, Object>>> mAppsProvider; 51 52 /** 53 * @param appsProvider items to add on the workspace 54 */ 55 public AddWorkspaceItemsTask(Provider<List<Pair<ItemInfo, Object>>> appsProvider) { 56 mAppsProvider = appsProvider; 57 } 58 59 @Override 60 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 61 List<Pair<ItemInfo, Object>> workspaceApps = mAppsProvider.get(); 62 if (workspaceApps.isEmpty()) { 63 return; 64 } 65 Context context = app.getContext(); 66 67 final ArrayList<ItemInfo> addedItemsFinal = new ArrayList<>(); 68 final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<>(); 69 ArrayMap<UserHandle, UserFolderInfo> userFolderMap = new ArrayMap<>(); 70 71 // Get the list of workspace screens. We need to append to this list and 72 // can not use sBgWorkspaceScreens because loadWorkspace() may not have been 73 // called. 74 ArrayList<Long> workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context); 75 synchronized(dataModel) { 76 77 List<ItemInfo> filteredItems = new ArrayList<>(); 78 for (Pair<ItemInfo, Object> entry : workspaceApps) { 79 ItemInfo item = entry.first; 80 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 81 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { 82 // Short-circuit this logic if the icon exists somewhere on the workspace 83 if (shortcutExists(dataModel, item.getIntent(), item.user)) { 84 continue; 85 } 86 } 87 88 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { 89 if (item instanceof AppInfo) { 90 item = ((AppInfo) item).makeShortcut(); 91 } 92 93 if (!Process.myUserHandle().equals(item.user)) { 94 // Check if this belongs to a work folder. 95 if (!(entry.second instanceof LauncherActivityInfo)) { 96 continue; 97 } 98 99 UserFolderInfo userFolderInfo = userFolderMap.get(item.user); 100 if (userFolderInfo == null) { 101 userFolderInfo = new UserFolderInfo(context, item.user, dataModel); 102 userFolderMap.put(item.user, userFolderInfo); 103 } 104 item = userFolderInfo.convertToWorkspaceItem( 105 (ShortcutInfo) item, (LauncherActivityInfo) entry.second); 106 } 107 } 108 if (item != null) { 109 filteredItems.add(item); 110 } 111 } 112 113 for (ItemInfo item : filteredItems) { 114 // Find appropriate space for the item. 115 Pair<Long, int[]> coords = findSpaceForItem(app, dataModel, workspaceScreens, 116 addedWorkspaceScreensFinal, item.spanX, item.spanY); 117 long screenId = coords.first; 118 int[] cordinates = coords.second; 119 120 ItemInfo itemInfo; 121 if (item instanceof ShortcutInfo || item instanceof FolderInfo || 122 item instanceof LauncherAppWidgetInfo) { 123 itemInfo = item; 124 } else if (item instanceof AppInfo) { 125 itemInfo = ((AppInfo) item).makeShortcut(); 126 } else { 127 throw new RuntimeException("Unexpected info type"); 128 } 129 130 // Add the shortcut to the db 131 getModelWriter().addItemToDatabase(itemInfo, 132 LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, 133 cordinates[0], cordinates[1]); 134 135 // Save the ShortcutInfo for binding in the workspace 136 addedItemsFinal.add(itemInfo); 137 } 138 } 139 140 // Update the workspace screens 141 updateScreens(context, workspaceScreens); 142 143 if (!addedItemsFinal.isEmpty()) { 144 scheduleCallbackTask(new CallbackTask() { 145 @Override 146 public void execute(Callbacks callbacks) { 147 final ArrayList<ItemInfo> addAnimated = new ArrayList<>(); 148 final ArrayList<ItemInfo> addNotAnimated = new ArrayList<>(); 149 if (!addedItemsFinal.isEmpty()) { 150 ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1); 151 long lastScreenId = info.screenId; 152 for (ItemInfo i : addedItemsFinal) { 153 if (i.screenId == lastScreenId) { 154 addAnimated.add(i); 155 } else { 156 addNotAnimated.add(i); 157 } 158 } 159 } 160 callbacks.bindAppsAdded(addedWorkspaceScreensFinal, 161 addNotAnimated, addAnimated); 162 } 163 }); 164 } 165 166 for (UserFolderInfo userFolderInfo : userFolderMap.values()) { 167 userFolderInfo.applyPendingState(getModelWriter()); 168 } 169 } 170 171 protected void updateScreens(Context context, ArrayList<Long> workspaceScreens) { 172 LauncherModel.updateWorkspaceScreenOrder(context, workspaceScreens); 173 } 174 175 /** 176 * Returns true if the shortcuts already exists on the workspace. This must be called after 177 * the workspace has been loaded. We identify a shortcut by its intent. 178 */ 179 protected boolean shortcutExists(BgDataModel dataModel, Intent intent, UserHandle user) { 180 final String compPkgName, intentWithPkg, intentWithoutPkg; 181 if (intent == null) { 182 // Skip items with null intents 183 return true; 184 } 185 if (intent.getComponent() != null) { 186 // If component is not null, an intent with null package will produce 187 // the same result and should also be a match. 188 compPkgName = intent.getComponent().getPackageName(); 189 if (intent.getPackage() != null) { 190 intentWithPkg = intent.toUri(0); 191 intentWithoutPkg = new Intent(intent).setPackage(null).toUri(0); 192 } else { 193 intentWithPkg = new Intent(intent).setPackage(compPkgName).toUri(0); 194 intentWithoutPkg = intent.toUri(0); 195 } 196 } else { 197 compPkgName = null; 198 intentWithPkg = intent.toUri(0); 199 intentWithoutPkg = intent.toUri(0); 200 } 201 202 boolean isLauncherAppTarget = Utilities.isLauncherAppTarget(intent); 203 synchronized (dataModel) { 204 for (ItemInfo item : dataModel.itemsIdMap) { 205 if (item instanceof ShortcutInfo) { 206 ShortcutInfo info = (ShortcutInfo) item; 207 if (item.getIntent() != null && info.user.equals(user)) { 208 Intent copyIntent = new Intent(item.getIntent()); 209 copyIntent.setSourceBounds(intent.getSourceBounds()); 210 String s = copyIntent.toUri(0); 211 if (intentWithPkg.equals(s) || intentWithoutPkg.equals(s)) { 212 return true; 213 } 214 215 // checking for existing promise icon with same package name 216 if (isLauncherAppTarget 217 && info.isPromise() 218 && info.hasStatusFlag(ShortcutInfo.FLAG_AUTOINSTALL_ICON) 219 && info.getTargetComponent() != null 220 && compPkgName != null 221 && compPkgName.equals(info.getTargetComponent().getPackageName())) { 222 return true; 223 } 224 } 225 } 226 } 227 } 228 return false; 229 } 230 231 /** 232 * Find a position on the screen for the given size or adds a new screen. 233 * @return screenId and the coordinates for the item. 234 */ 235 protected Pair<Long, int[]> findSpaceForItem( 236 LauncherAppState app, BgDataModel dataModel, 237 ArrayList<Long> workspaceScreens, 238 ArrayList<Long> addedWorkspaceScreensFinal, 239 int spanX, int spanY) { 240 LongSparseArray<ArrayList<ItemInfo>> screenItems = new LongSparseArray<>(); 241 242 // Use sBgItemsIdMap as all the items are already loaded. 243 synchronized (dataModel) { 244 for (ItemInfo info : dataModel.itemsIdMap) { 245 if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 246 ArrayList<ItemInfo> items = screenItems.get(info.screenId); 247 if (items == null) { 248 items = new ArrayList<>(); 249 screenItems.put(info.screenId, items); 250 } 251 items.add(info); 252 } 253 } 254 } 255 256 // Find appropriate space for the item. 257 long screenId = 0; 258 int[] cordinates = new int[2]; 259 boolean found = false; 260 261 int screenCount = workspaceScreens.size(); 262 // First check the preferred screen. 263 int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1; 264 if (preferredScreenIndex < screenCount) { 265 screenId = workspaceScreens.get(preferredScreenIndex); 266 found = findNextAvailableIconSpaceInScreen( 267 app, screenItems.get(screenId), cordinates, spanX, spanY); 268 } 269 270 if (!found) { 271 // Search on any of the screens starting from the first screen. 272 for (int screen = 1; screen < screenCount; screen++) { 273 screenId = workspaceScreens.get(screen); 274 if (findNextAvailableIconSpaceInScreen( 275 app, screenItems.get(screenId), cordinates, spanX, spanY)) { 276 // We found a space for it 277 found = true; 278 break; 279 } 280 } 281 } 282 283 if (!found) { 284 // Still no position found. Add a new screen to the end. 285 screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(), 286 LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) 287 .getLong(LauncherSettings.Settings.EXTRA_VALUE); 288 289 // Save the screen id for binding in the workspace 290 workspaceScreens.add(screenId); 291 addedWorkspaceScreensFinal.add(screenId); 292 293 // If we still can't find an empty space, then God help us all!!! 294 if (!findNextAvailableIconSpaceInScreen( 295 app, screenItems.get(screenId), cordinates, spanX, spanY)) { 296 throw new RuntimeException("Can't find space to add the item"); 297 } 298 } 299 return Pair.create(screenId, cordinates); 300 } 301 302 private boolean findNextAvailableIconSpaceInScreen( 303 LauncherAppState app, ArrayList<ItemInfo> occupiedPos, 304 int[] xy, int spanX, int spanY) { 305 InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); 306 307 GridOccupancy occupied = new GridOccupancy(profile.numColumns, profile.numRows); 308 if (occupiedPos != null) { 309 for (ItemInfo r : occupiedPos) { 310 occupied.markCells(r, true); 311 } 312 } 313 return occupied.findVacantCell(xy, spanX, spanY); 314 } 315 316} 317