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 static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING; 20 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; 21 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 22 23 import android.os.Process; 24 import android.util.Log; 25 26 import com.android.launcher3.InvariantDeviceProfile; 27 import com.android.launcher3.LauncherAppState; 28 import com.android.launcher3.LauncherModel.CallbackTask; 29 import com.android.launcher3.LauncherSettings; 30 import com.android.launcher3.Workspace; 31 import com.android.launcher3.config.FeatureFlags; 32 import com.android.launcher3.model.BgDataModel.Callbacks; 33 import com.android.launcher3.model.BgDataModel.FixedContainerItems; 34 import com.android.launcher3.model.data.AppInfo; 35 import com.android.launcher3.model.data.ItemInfo; 36 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 37 import com.android.launcher3.testing.shared.TestProtocol; 38 import com.android.launcher3.util.IntArray; 39 import com.android.launcher3.util.IntSet; 40 import com.android.launcher3.util.LooperExecutor; 41 import com.android.launcher3.util.LooperIdleLock; 42 import com.android.launcher3.util.RunnableList; 43 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.Set; 50 import java.util.concurrent.Executor; 51 52 /** 53 * Binds the results of {@link com.android.launcher3.model.LoaderTask} to the Callbacks objects. 54 */ 55 public abstract class BaseLauncherBinder { 56 57 protected static final String TAG = "LauncherBinder"; 58 private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons 59 60 protected final LooperExecutor mUiExecutor; 61 62 protected final LauncherAppState mApp; 63 protected final BgDataModel mBgDataModel; 64 private final AllAppsList mBgAllAppsList; 65 66 private final Callbacks[] mCallbacksList; 67 68 private int mMyBindingId; 69 BaseLauncherBinder(LauncherAppState app, BgDataModel dataModel, AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor)70 public BaseLauncherBinder(LauncherAppState app, BgDataModel dataModel, 71 AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) { 72 mUiExecutor = uiExecutor; 73 mApp = app; 74 mBgDataModel = dataModel; 75 mBgAllAppsList = allAppsList; 76 mCallbacksList = callbacksList; 77 } 78 79 /** 80 * Binds all loaded data to actual views on the main thread. 81 */ bindWorkspace(boolean incrementBindId)82 public void bindWorkspace(boolean incrementBindId) { 83 if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) { 84 DisjointWorkspaceBinder workspaceBinder = 85 initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens()); 86 workspaceBinder.bindCurrentWorkspacePages(); 87 workspaceBinder.bindOtherWorkspacePages(); 88 } else { 89 bindWorkspaceAllAtOnce(incrementBindId); 90 } 91 } 92 93 /** 94 * Initializes the WorkspaceBinder for binding. 95 * 96 * @param incrementBindId this is used to stop previously started binding tasks that are 97 * obsolete but still queued. 98 * @param workspacePages this allows the Launcher to add the correct workspace screens. 99 */ initWorkspaceBinder(boolean incrementBindId, IntArray workspacePages)100 public DisjointWorkspaceBinder initWorkspaceBinder(boolean incrementBindId, 101 IntArray workspacePages) { 102 103 synchronized (mBgDataModel) { 104 if (incrementBindId) { 105 mBgDataModel.lastBindId++; 106 } 107 mMyBindingId = mBgDataModel.lastBindId; 108 return new DisjointWorkspaceBinder(workspacePages); 109 } 110 } 111 bindWorkspaceAllAtOnce(boolean incrementBindId)112 private void bindWorkspaceAllAtOnce(boolean incrementBindId) { 113 // Save a copy of all the bg-thread collections 114 ArrayList<ItemInfo> workspaceItems = new ArrayList<>(); 115 ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>(); 116 final IntArray orderedScreenIds = new IntArray(); 117 ArrayList<FixedContainerItems> extraItems = new ArrayList<>(); 118 119 synchronized (mBgDataModel) { 120 workspaceItems.addAll(mBgDataModel.workspaceItems); 121 appWidgets.addAll(mBgDataModel.appWidgets); 122 orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens()); 123 mBgDataModel.extraItems.forEach(extraItems::add); 124 if (incrementBindId) { 125 mBgDataModel.lastBindId++; 126 } 127 mMyBindingId = mBgDataModel.lastBindId; 128 } 129 130 for (Callbacks cb : mCallbacksList) { 131 new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId, 132 workspaceItems, appWidgets, extraItems, orderedScreenIds).bind(); 133 } 134 } 135 136 /** 137 * BindDeepShortcuts is abstract because it is a no-op for the go launcher. 138 */ bindDeepShortcuts()139 public abstract void bindDeepShortcuts(); 140 141 /** 142 * Binds the all apps results from LoaderTask to the callbacks UX. 143 */ bindAllApps()144 public void bindAllApps() { 145 // shallow copy 146 AppInfo[] apps = mBgAllAppsList.copyData(); 147 int flags = mBgAllAppsList.getFlags(); 148 executeCallbacksTask(c -> c.bindAllApplications(apps, flags), mUiExecutor); 149 } 150 151 /** 152 * bindWidgets is abstract because it is a no-op for the go launcher. 153 */ bindWidgets()154 public abstract void bindWidgets(); 155 156 /** 157 * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right) 158 */ sortWorkspaceItemsSpatially(InvariantDeviceProfile profile, ArrayList<ItemInfo> workspaceItems)159 protected void sortWorkspaceItemsSpatially(InvariantDeviceProfile profile, 160 ArrayList<ItemInfo> workspaceItems) { 161 final int screenCols = profile.numColumns; 162 final int screenCellCount = profile.numColumns * profile.numRows; 163 Collections.sort(workspaceItems, (lhs, rhs) -> { 164 if (lhs.container == rhs.container) { 165 // Within containers, order by their spatial position in that container 166 switch (lhs.container) { 167 case LauncherSettings.Favorites.CONTAINER_DESKTOP: { 168 int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols 169 + lhs.cellX); 170 int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols 171 + rhs.cellX); 172 return Integer.compare(lr, rr); 173 } 174 case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { 175 // We currently use the screen id as the rank 176 return Integer.compare(lhs.screenId, rhs.screenId); 177 } 178 default: 179 if (FeatureFlags.IS_STUDIO_BUILD) { 180 throw new RuntimeException( 181 "Unexpected container type when sorting workspace items."); 182 } 183 return 0; 184 } 185 } else { 186 // Between containers, order by hotseat, desktop 187 return Integer.compare(lhs.container, rhs.container); 188 } 189 }); 190 } 191 executeCallbacksTask(CallbackTask task, Executor executor)192 protected void executeCallbacksTask(CallbackTask task, Executor executor) { 193 executor.execute(() -> { 194 if (mMyBindingId != mBgDataModel.lastBindId) { 195 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind"); 196 return; 197 } 198 for (Callbacks cb : mCallbacksList) { 199 task.execute(cb); 200 } 201 }); 202 } 203 204 /** 205 * Only used in LoaderTask. 206 */ newIdleLock(Object lock)207 public LooperIdleLock newIdleLock(Object lock) { 208 LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper()); 209 // If we are not binding or if the main looper is already idle, there is no reason to wait 210 if (mUiExecutor.getLooper().getQueue().isIdle()) { 211 idleLock.queueIdle(); 212 } 213 return idleLock; 214 } 215 216 private class UnifiedWorkspaceBinder { 217 218 private final Executor mUiExecutor; 219 private final Callbacks mCallbacks; 220 221 private final LauncherAppState mApp; 222 private final BgDataModel mBgDataModel; 223 224 private final int mMyBindingId; 225 private final ArrayList<ItemInfo> mWorkspaceItems; 226 private final ArrayList<LauncherAppWidgetInfo> mAppWidgets; 227 private final IntArray mOrderedScreenIds; 228 private final ArrayList<FixedContainerItems> mExtraItems; 229 UnifiedWorkspaceBinder(Callbacks callbacks, Executor uiExecutor, LauncherAppState app, BgDataModel bgDataModel, int myBindingId, ArrayList<ItemInfo> workspaceItems, ArrayList<LauncherAppWidgetInfo> appWidgets, ArrayList<FixedContainerItems> extraItems, IntArray orderedScreenIds)230 UnifiedWorkspaceBinder(Callbacks callbacks, 231 Executor uiExecutor, 232 LauncherAppState app, 233 BgDataModel bgDataModel, 234 int myBindingId, 235 ArrayList<ItemInfo> workspaceItems, 236 ArrayList<LauncherAppWidgetInfo> appWidgets, 237 ArrayList<FixedContainerItems> extraItems, 238 IntArray orderedScreenIds) { 239 mCallbacks = callbacks; 240 mUiExecutor = uiExecutor; 241 mApp = app; 242 mBgDataModel = bgDataModel; 243 mMyBindingId = myBindingId; 244 mWorkspaceItems = workspaceItems; 245 mAppWidgets = appWidgets; 246 mExtraItems = extraItems; 247 mOrderedScreenIds = orderedScreenIds; 248 } 249 bind()250 private void bind() { 251 final IntSet currentScreenIds = 252 mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds); 253 Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks); 254 255 // Separate the items that are on the current screen, and all the other remaining items 256 ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>(); 257 ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>(); 258 ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>(); 259 ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>(); 260 261 if (TestProtocol.sDebugTracing) { 262 Log.d(TestProtocol.NULL_INT_SET, "bind (1) currentScreenIds: " 263 + currentScreenIds 264 + ", pointer: " 265 + mCallbacks 266 + ", name: " 267 + mCallbacks.getClass().getName()); 268 } 269 filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems, 270 otherWorkspaceItems); 271 if (TestProtocol.sDebugTracing) { 272 Log.d(TestProtocol.NULL_INT_SET, "bind (2) currentScreenIds: " 273 + currentScreenIds); 274 } 275 filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets, 276 otherAppWidgets); 277 final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); 278 sortWorkspaceItemsSpatially(idp, currentWorkspaceItems); 279 sortWorkspaceItemsSpatially(idp, otherWorkspaceItems); 280 281 // Tell the workspace that we're about to start binding items 282 if (TestProtocol.sDebugTracing) { 283 Log.d(TestProtocol.FLAKY_BINDING, "scheduling: startBinding"); 284 } 285 executeCallbacksTask(c -> { 286 c.clearPendingBinds(); 287 c.startBinding(); 288 }, mUiExecutor); 289 290 // Bind workspace screens 291 executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); 292 293 // Load items on the current page. 294 bindWorkspaceItems(currentWorkspaceItems, mUiExecutor); 295 bindAppWidgets(currentAppWidgets, mUiExecutor); 296 mExtraItems.forEach(item -> 297 executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor)); 298 299 RunnableList pendingTasks = new RunnableList(); 300 Executor pendingExecutor = pendingTasks::add; 301 bindWorkspaceItems(otherWorkspaceItems, pendingExecutor); 302 bindAppWidgets(otherAppWidgets, pendingExecutor); 303 if (TestProtocol.sDebugTracing) { 304 Log.d(TestProtocol.FLAKY_BINDING, "scheduling: finishBindingItems"); 305 } 306 executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor); 307 pendingExecutor.execute( 308 () -> { 309 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); 310 ItemInstallQueue.INSTANCE.get(mApp.getContext()) 311 .resumeModelPush(FLAG_LOADER_RUNNING); 312 }); 313 314 executeCallbacksTask( 315 c -> { 316 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 317 c.onInitialBindComplete(currentScreenIds, pendingTasks); 318 }, mUiExecutor); 319 320 mCallbacks.bindStringCache(mBgDataModel.stringCache.clone()); 321 } 322 bindWorkspaceItems( final ArrayList<ItemInfo> workspaceItems, final Executor executor)323 private void bindWorkspaceItems( 324 final ArrayList<ItemInfo> workspaceItems, final Executor executor) { 325 // Bind the workspace items 326 int count = workspaceItems.size(); 327 for (int i = 0; i < count; i += ITEMS_CHUNK) { 328 final int start = i; 329 final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i); 330 executeCallbacksTask( 331 c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false), 332 executor); 333 } 334 } 335 bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor)336 private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) { 337 // Bind the widgets, one at a time 338 int count = appWidgets.size(); 339 for (int i = 0; i < count; i++) { 340 final ItemInfo widget = appWidgets.get(i); 341 executeCallbacksTask( 342 c -> c.bindItems(Collections.singletonList(widget), false), executor); 343 } 344 } 345 executeCallbacksTask(CallbackTask task, Executor executor)346 protected void executeCallbacksTask(CallbackTask task, Executor executor) { 347 executor.execute(() -> { 348 if (mMyBindingId != mBgDataModel.lastBindId) { 349 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind"); 350 return; 351 } 352 task.execute(mCallbacks); 353 }); 354 } 355 } 356 357 private class DisjointWorkspaceBinder { 358 private final IntArray mOrderedScreenIds; 359 private final IntSet mCurrentScreenIds = new IntSet(); 360 private final Set<Integer> mBoundItemIds = new HashSet<>(); 361 DisjointWorkspaceBinder(IntArray orderedScreenIds)362 protected DisjointWorkspaceBinder(IntArray orderedScreenIds) { 363 mOrderedScreenIds = orderedScreenIds; 364 365 for (Callbacks cb : mCallbacksList) { 366 mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds)); 367 } 368 if (mCurrentScreenIds.size() == 0) { 369 mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID); 370 } 371 } 372 373 /** 374 * Binds the currently loaded items in the Data Model. Also signals to the Callbacks[] 375 * that these items have been bound and their respective screens are ready to be shown. 376 * 377 * If this method is called after all the items on the workspace screen have already been 378 * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will 379 * not bind any items. 380 */ bindCurrentWorkspacePages()381 protected void bindCurrentWorkspacePages() { 382 // Save a copy of all the bg-thread collections 383 ArrayList<ItemInfo> workspaceItems; 384 ArrayList<LauncherAppWidgetInfo> appWidgets; 385 386 synchronized (mBgDataModel) { 387 workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems); 388 appWidgets = new ArrayList<>(mBgDataModel.appWidgets); 389 } 390 391 workspaceItems.forEach(it -> mBoundItemIds.add(it.id)); 392 appWidgets.forEach(it -> mBoundItemIds.add(it.id)); 393 394 sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems); 395 396 // Tell the workspace that we're about to start binding items 397 executeCallbacksTask(c -> { 398 c.clearPendingBinds(); 399 c.startBinding(); 400 }, mUiExecutor); 401 402 // Bind workspace screens 403 executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); 404 405 bindWorkspaceItems(workspaceItems); 406 bindAppWidgets(appWidgets); 407 408 executeCallbacksTask(c -> { 409 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 410 c.onInitialBindComplete(mCurrentScreenIds, new RunnableList()); 411 }, mUiExecutor); 412 } 413 bindOtherWorkspacePages()414 protected void bindOtherWorkspacePages() { 415 // Save a copy of all the bg-thread collections 416 ArrayList<ItemInfo> workspaceItems; 417 ArrayList<LauncherAppWidgetInfo> appWidgets; 418 419 synchronized (mBgDataModel) { 420 workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems); 421 appWidgets = new ArrayList<>(mBgDataModel.appWidgets); 422 } 423 424 workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id)); 425 appWidgets.removeIf(it -> mBoundItemIds.contains(it.id)); 426 427 sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems); 428 429 bindWorkspaceItems(workspaceItems); 430 bindAppWidgets(appWidgets); 431 432 executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor); 433 mUiExecutor.execute(() -> { 434 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); 435 ItemInstallQueue.INSTANCE.get(mApp.getContext()) 436 .resumeModelPush(FLAG_LOADER_RUNNING); 437 }); 438 439 for (Callbacks cb : mCallbacksList) { 440 cb.bindStringCache(mBgDataModel.stringCache.clone()); 441 } 442 } 443 bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems)444 private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) { 445 // Bind the workspace items 446 int count = workspaceItems.size(); 447 for (int i = 0; i < count; i += ITEMS_CHUNK) { 448 final int start = i; 449 final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i); 450 executeCallbacksTask( 451 c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false), 452 mUiExecutor); 453 } 454 } 455 bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets)456 private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) { 457 // Bind the widgets, one at a time 458 int count = appWidgets.size(); 459 for (int i = 0; i < count; i++) { 460 final ItemInfo widget = appWidgets.get(i); 461 executeCallbacksTask( 462 c -> c.bindItems(Collections.singletonList(widget), false), 463 mUiExecutor); 464 } 465 } 466 } 467 } 468