1 /* 2 * Copyright (C) 2017 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 17 package com.android.launcher3.model; 18 19 import android.os.Looper; 20 import android.util.Log; 21 22 import com.android.launcher3.AllAppsList; 23 import com.android.launcher3.AppInfo; 24 import com.android.launcher3.InvariantDeviceProfile; 25 import com.android.launcher3.ItemInfo; 26 import com.android.launcher3.LauncherAppState; 27 import com.android.launcher3.LauncherAppWidgetInfo; 28 import com.android.launcher3.LauncherModel; 29 import com.android.launcher3.LauncherModel.Callbacks; 30 import com.android.launcher3.LauncherSettings; 31 import com.android.launcher3.MainThreadExecutor; 32 import com.android.launcher3.PagedView; 33 import com.android.launcher3.Utilities; 34 import com.android.launcher3.config.FeatureFlags; 35 import com.android.launcher3.util.ComponentKey; 36 import com.android.launcher3.util.LooperIdleLock; 37 import com.android.launcher3.util.MultiHashMap; 38 import com.android.launcher3.util.ViewOnDrawExecutor; 39 40 import java.lang.ref.WeakReference; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.HashSet; 45 import java.util.Iterator; 46 import java.util.Set; 47 import java.util.concurrent.Executor; 48 49 /** 50 * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}. 51 */ 52 public class LoaderResults { 53 54 private static final String TAG = "LoaderResults"; 55 private static final long INVALID_SCREEN_ID = -1L; 56 private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons 57 58 private final Executor mUiExecutor; 59 60 private final LauncherAppState mApp; 61 private final BgDataModel mBgDataModel; 62 private final AllAppsList mBgAllAppsList; 63 private final int mPageToBindFirst; 64 65 private final WeakReference<Callbacks> mCallbacks; 66 LoaderResults(LauncherAppState app, BgDataModel dataModel, AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks)67 public LoaderResults(LauncherAppState app, BgDataModel dataModel, 68 AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) { 69 mUiExecutor = new MainThreadExecutor(); 70 mApp = app; 71 mBgDataModel = dataModel; 72 mBgAllAppsList = allAppsList; 73 mPageToBindFirst = pageToBindFirst; 74 mCallbacks = callbacks == null ? new WeakReference<Callbacks>(null) : callbacks; 75 } 76 77 /** 78 * Binds all loaded data to actual views on the main thread. 79 */ bindWorkspace()80 public void bindWorkspace() { 81 Runnable r; 82 83 Callbacks callbacks = mCallbacks.get(); 84 // Don't use these two variables in any of the callback runnables. 85 // Otherwise we hold a reference to them. 86 if (callbacks == null) { 87 // This launcher has exited and nobody bothered to tell us. Just bail. 88 Log.w(TAG, "LoaderTask running with no launcher"); 89 return; 90 } 91 92 // Save a copy of all the bg-thread collections 93 ArrayList<ItemInfo> workspaceItems = new ArrayList<>(); 94 ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>(); 95 final ArrayList<Long> orderedScreenIds = new ArrayList<>(); 96 97 synchronized (mBgDataModel) { 98 workspaceItems.addAll(mBgDataModel.workspaceItems); 99 appWidgets.addAll(mBgDataModel.appWidgets); 100 orderedScreenIds.addAll(mBgDataModel.workspaceScreens); 101 } 102 103 final int currentScreen; 104 { 105 int currScreen = mPageToBindFirst != PagedView.INVALID_RESTORE_PAGE 106 ? mPageToBindFirst : callbacks.getCurrentWorkspaceScreen(); 107 if (currScreen >= orderedScreenIds.size()) { 108 // There may be no workspace screens (just hotseat items and an empty page). 109 currScreen = PagedView.INVALID_RESTORE_PAGE; 110 } 111 currentScreen = currScreen; 112 } 113 final boolean validFirstPage = currentScreen >= 0; 114 final long currentScreenId = 115 validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID; 116 117 // Separate the items that are on the current screen, and all the other remaining items 118 ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>(); 119 ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>(); 120 ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>(); 121 ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>(); 122 123 filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems, 124 otherWorkspaceItems); 125 filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets, 126 otherAppWidgets); 127 sortWorkspaceItemsSpatially(currentWorkspaceItems); 128 sortWorkspaceItemsSpatially(otherWorkspaceItems); 129 130 // Tell the workspace that we're about to start binding items 131 r = new Runnable() { 132 public void run() { 133 Callbacks callbacks = mCallbacks.get(); 134 if (callbacks != null) { 135 callbacks.clearPendingBinds(); 136 callbacks.startBinding(); 137 } 138 } 139 }; 140 mUiExecutor.execute(r); 141 142 // Bind workspace screens 143 mUiExecutor.execute(new Runnable() { 144 @Override 145 public void run() { 146 Callbacks callbacks = mCallbacks.get(); 147 if (callbacks != null) { 148 callbacks.bindScreens(orderedScreenIds); 149 } 150 } 151 }); 152 153 Executor mainExecutor = mUiExecutor; 154 // Load items on the current page. 155 bindWorkspaceItems(currentWorkspaceItems, currentAppWidgets, mainExecutor); 156 157 // In case of validFirstPage, only bind the first screen, and defer binding the 158 // remaining screens after first onDraw (and an optional the fade animation whichever 159 // happens later). 160 // This ensures that the first screen is immediately visible (eg. during rotation) 161 // In case of !validFirstPage, bind all pages one after other. 162 final Executor deferredExecutor = 163 validFirstPage ? new ViewOnDrawExecutor(mUiExecutor) : mainExecutor; 164 165 mainExecutor.execute(new Runnable() { 166 @Override 167 public void run() { 168 Callbacks callbacks = mCallbacks.get(); 169 if (callbacks != null) { 170 callbacks.finishFirstPageBind( 171 validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null); 172 } 173 } 174 }); 175 176 bindWorkspaceItems(otherWorkspaceItems, otherAppWidgets, deferredExecutor); 177 178 // Tell the workspace that we're done binding items 179 r = new Runnable() { 180 public void run() { 181 Callbacks callbacks = mCallbacks.get(); 182 if (callbacks != null) { 183 callbacks.finishBindingItems(); 184 } 185 } 186 }; 187 deferredExecutor.execute(r); 188 189 if (validFirstPage) { 190 r = new Runnable() { 191 public void run() { 192 Callbacks callbacks = mCallbacks.get(); 193 if (callbacks != null) { 194 // We are loading synchronously, which means, some of the pages will be 195 // bound after first draw. Inform the callbacks that page binding is 196 // not complete, and schedule the remaining pages. 197 if (currentScreen != PagedView.INVALID_RESTORE_PAGE) { 198 callbacks.onPageBoundSynchronously(currentScreen); 199 } 200 callbacks.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor); 201 } 202 } 203 }; 204 mUiExecutor.execute(r); 205 } 206 } 207 208 209 /** Filters the set of items who are directly or indirectly (via another container) on the 210 * specified screen. */ filterCurrentWorkspaceItems(long currentScreenId, ArrayList<T> allWorkspaceItems, ArrayList<T> currentScreenItems, ArrayList<T> otherScreenItems)211 private <T extends ItemInfo> void filterCurrentWorkspaceItems(long currentScreenId, 212 ArrayList<T> allWorkspaceItems, 213 ArrayList<T> currentScreenItems, 214 ArrayList<T> otherScreenItems) { 215 // Purge any null ItemInfos 216 Iterator<T> iter = allWorkspaceItems.iterator(); 217 while (iter.hasNext()) { 218 ItemInfo i = iter.next(); 219 if (i == null) { 220 iter.remove(); 221 } 222 } 223 224 // Order the set of items by their containers first, this allows use to walk through the 225 // list sequentially, build up a list of containers that are in the specified screen, 226 // as well as all items in those containers. 227 Set<Long> itemsOnScreen = new HashSet<>(); 228 Collections.sort(allWorkspaceItems, new Comparator<ItemInfo>() { 229 @Override 230 public int compare(ItemInfo lhs, ItemInfo rhs) { 231 return Utilities.longCompare(lhs.container, rhs.container); 232 } 233 }); 234 for (T info : allWorkspaceItems) { 235 if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 236 if (info.screenId == currentScreenId) { 237 currentScreenItems.add(info); 238 itemsOnScreen.add(info.id); 239 } else { 240 otherScreenItems.add(info); 241 } 242 } else if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { 243 currentScreenItems.add(info); 244 itemsOnScreen.add(info.id); 245 } else { 246 if (itemsOnScreen.contains(info.container)) { 247 currentScreenItems.add(info); 248 itemsOnScreen.add(info.id); 249 } else { 250 otherScreenItems.add(info); 251 } 252 } 253 } 254 } 255 256 /** Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to 257 * right) */ sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems)258 private void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) { 259 final InvariantDeviceProfile profile = mApp.getInvariantDeviceProfile(); 260 final int screenCols = profile.numColumns; 261 final int screenCellCount = profile.numColumns * profile.numRows; 262 Collections.sort(workspaceItems, new Comparator<ItemInfo>() { 263 @Override 264 public int compare(ItemInfo lhs, ItemInfo rhs) { 265 if (lhs.container == rhs.container) { 266 // Within containers, order by their spatial position in that container 267 switch ((int) lhs.container) { 268 case LauncherSettings.Favorites.CONTAINER_DESKTOP: { 269 long lr = (lhs.screenId * screenCellCount + 270 lhs.cellY * screenCols + lhs.cellX); 271 long rr = (rhs.screenId * screenCellCount + 272 rhs.cellY * screenCols + rhs.cellX); 273 return Utilities.longCompare(lr, rr); 274 } 275 case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { 276 // We currently use the screen id as the rank 277 return Utilities.longCompare(lhs.screenId, rhs.screenId); 278 } 279 default: 280 if (FeatureFlags.IS_DOGFOOD_BUILD) { 281 throw new RuntimeException("Unexpected container type when " + 282 "sorting workspace items."); 283 } 284 return 0; 285 } 286 } else { 287 // Between containers, order by hotseat, desktop 288 return Utilities.longCompare(lhs.container, rhs.container); 289 } 290 } 291 }); 292 } 293 bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems, final ArrayList<LauncherAppWidgetInfo> appWidgets, final Executor executor)294 private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems, 295 final ArrayList<LauncherAppWidgetInfo> appWidgets, 296 final Executor executor) { 297 298 // Bind the workspace items 299 int N = workspaceItems.size(); 300 for (int i = 0; i < N; i += ITEMS_CHUNK) { 301 final int start = i; 302 final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i); 303 final Runnable r = new Runnable() { 304 @Override 305 public void run() { 306 Callbacks callbacks = mCallbacks.get(); 307 if (callbacks != null) { 308 callbacks.bindItems(workspaceItems.subList(start, start+chunkSize), false); 309 } 310 } 311 }; 312 executor.execute(r); 313 } 314 315 // Bind the widgets, one at a time 316 N = appWidgets.size(); 317 for (int i = 0; i < N; i++) { 318 final ItemInfo widget = appWidgets.get(i); 319 final Runnable r = new Runnable() { 320 public void run() { 321 Callbacks callbacks = mCallbacks.get(); 322 if (callbacks != null) { 323 callbacks.bindItems(Collections.singletonList(widget), false); 324 } 325 } 326 }; 327 executor.execute(r); 328 } 329 } 330 bindDeepShortcuts()331 public void bindDeepShortcuts() { 332 final MultiHashMap<ComponentKey, String> shortcutMapCopy; 333 synchronized (mBgDataModel) { 334 shortcutMapCopy = mBgDataModel.deepShortcutMap.clone(); 335 } 336 Runnable r = new Runnable() { 337 @Override 338 public void run() { 339 Callbacks callbacks = mCallbacks.get(); 340 if (callbacks != null) { 341 callbacks.bindDeepShortcutMap(shortcutMapCopy); 342 } 343 } 344 }; 345 mUiExecutor.execute(r); 346 } 347 bindAllApps()348 public void bindAllApps() { 349 // shallow copy 350 @SuppressWarnings("unchecked") 351 final ArrayList<AppInfo> list = (ArrayList<AppInfo>) mBgAllAppsList.data.clone(); 352 353 Runnable r = new Runnable() { 354 public void run() { 355 Callbacks callbacks = mCallbacks.get(); 356 if (callbacks != null) { 357 callbacks.bindAllApplications(list); 358 } 359 } 360 }; 361 mUiExecutor.execute(r); 362 } 363 bindWidgets()364 public void bindWidgets() { 365 final MultiHashMap<PackageItemInfo, WidgetItem> widgets 366 = mBgDataModel.widgetsModel.getWidgetsMap(); 367 Runnable r = new Runnable() { 368 public void run() { 369 Callbacks callbacks = mCallbacks.get(); 370 if (callbacks != null) { 371 callbacks.bindAllWidgets(widgets); 372 } 373 } 374 }; 375 mUiExecutor.execute(r); 376 } 377 newIdleLock(Object lock)378 public LooperIdleLock newIdleLock(Object lock) { 379 LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper()); 380 // If we are not binding, there is no reason to wait for idle. 381 if (mCallbacks.get() == null) { 382 idleLock.queueIdle(); 383 } 384 return idleLock; 385 } 386 } 387