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