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