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.BuildConfig.WIDGETS_ENABLED; 20 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle; 21 import static com.android.launcher3.Flags.enableWorkspaceInflation; 22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; 23 import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING; 24 import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER; 25 import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter; 26 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 27 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 28 29 import android.content.Context; 30 import android.os.Trace; 31 import android.util.Log; 32 import android.util.Pair; 33 import android.view.View; 34 35 import androidx.annotation.NonNull; 36 37 import com.android.launcher3.InvariantDeviceProfile; 38 import com.android.launcher3.LauncherModel; 39 import com.android.launcher3.LauncherModel.CallbackTask; 40 import com.android.launcher3.LauncherSettings; 41 import com.android.launcher3.celllayout.CellPosMapper; 42 import com.android.launcher3.config.FeatureFlags; 43 import com.android.launcher3.dagger.ApplicationContext; 44 import com.android.launcher3.model.BgDataModel.Callbacks; 45 import com.android.launcher3.model.BgDataModel.FixedContainerItems; 46 import com.android.launcher3.model.data.AppInfo; 47 import com.android.launcher3.model.data.ItemInfo; 48 import com.android.launcher3.util.ComponentKey; 49 import com.android.launcher3.util.IntArray; 50 import com.android.launcher3.util.IntSet; 51 import com.android.launcher3.util.IntSparseArrayMap; 52 import com.android.launcher3.util.ItemInflater; 53 import com.android.launcher3.util.LooperExecutor; 54 import com.android.launcher3.util.LooperIdleLock; 55 import com.android.launcher3.util.PackageUserKey; 56 import com.android.launcher3.util.RunnableList; 57 import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder; 58 import com.android.launcher3.widget.model.WidgetsListBaseEntry; 59 60 import dagger.assisted.Assisted; 61 import dagger.assisted.AssistedFactory; 62 import dagger.assisted.AssistedInject; 63 64 import java.util.ArrayList; 65 import java.util.Arrays; 66 import java.util.Collections; 67 import java.util.HashMap; 68 import java.util.List; 69 import java.util.Map; 70 import java.util.Objects; 71 import java.util.concurrent.Executor; 72 import java.util.function.Predicate; 73 import java.util.stream.Collectors; 74 75 /** 76 * Binds the results of {@link com.android.launcher3.model.LoaderTask} to the Callbacks objects. 77 */ 78 public class BaseLauncherBinder { 79 80 protected static final String TAG = "LauncherBinder"; 81 private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons 82 83 protected final LooperExecutor mUiExecutor; 84 85 private final Context mContext; 86 private final InvariantDeviceProfile mIDP; 87 private final LauncherModel mModel; 88 protected final BgDataModel mBgDataModel; 89 private final AllAppsList mBgAllAppsList; 90 91 final Callbacks[] mCallbacksList; 92 93 private int mMyBindingId; 94 95 @AssistedInject BaseLauncherBinder( @pplicationContext Context context, InvariantDeviceProfile idp, LauncherModel model, BgDataModel dataModel, AllAppsList allAppsList, @Assisted Callbacks[] callbacksList)96 public BaseLauncherBinder( 97 @ApplicationContext Context context, 98 InvariantDeviceProfile idp, 99 LauncherModel model, 100 BgDataModel dataModel, 101 AllAppsList allAppsList, 102 @Assisted Callbacks[] callbacksList) { 103 mUiExecutor = MAIN_EXECUTOR; 104 mContext = context; 105 mIDP = idp; 106 mModel = model; 107 mBgDataModel = dataModel; 108 mBgAllAppsList = allAppsList; 109 mCallbacksList = callbacksList; 110 } 111 112 /** 113 * Binds all loaded data to actual views on the main thread. 114 */ bindWorkspace(boolean incrementBindId, boolean isBindSync)115 public void bindWorkspace(boolean incrementBindId, boolean isBindSync) { 116 Trace.beginSection("BaseLauncherBinder#bindWorkspace"); 117 try { 118 // Save a copy of all the bg-thread collections 119 IntSparseArrayMap<ItemInfo> itemsIdMap; 120 final IntArray orderedScreenIds = new IntArray(); 121 ArrayList<FixedContainerItems> extraItems = new ArrayList<>(); 122 final int workspaceItemCount; 123 synchronized (mBgDataModel) { 124 itemsIdMap = mBgDataModel.itemsIdMap.clone(); 125 orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens()); 126 mBgDataModel.extraItems.forEach(extraItems::add); 127 if (incrementBindId) { 128 mBgDataModel.lastBindId++; 129 mBgDataModel.lastLoadId = mModel.getLastLoadId(); 130 } 131 mMyBindingId = mBgDataModel.lastBindId; 132 workspaceItemCount = mBgDataModel.itemsIdMap.size(); 133 } 134 135 for (Callbacks cb : mCallbacksList) { 136 new UnifiedWorkspaceBinder(cb, itemsIdMap, extraItems, orderedScreenIds) 137 .bind(isBindSync, workspaceItemCount); 138 } 139 } finally { 140 Trace.endSection(); 141 } 142 } 143 144 /** 145 * BindDeepShortcuts is abstract because it is a no-op for the go launcher. 146 */ bindDeepShortcuts()147 public void bindDeepShortcuts() { 148 if (!WIDGETS_ENABLED) { 149 return; 150 } 151 final HashMap<ComponentKey, Integer> shortcutMapCopy; 152 synchronized (mBgDataModel) { 153 shortcutMapCopy = new HashMap<>(mBgDataModel.deepShortcutMap); 154 } 155 executeCallbacksTask(c -> c.bindDeepShortcutMap(shortcutMapCopy), mUiExecutor); 156 } 157 158 /** 159 * Binds the all apps results from LoaderTask to the callbacks UX. 160 */ bindAllApps()161 public void bindAllApps() { 162 // shallow copy 163 AppInfo[] apps = mBgAllAppsList.copyData(); 164 int flags = mBgAllAppsList.getFlags(); 165 Map<PackageUserKey, Integer> packageUserKeytoUidMap = Arrays.stream(apps).collect( 166 Collectors.toMap( 167 appInfo -> new PackageUserKey(appInfo.componentName.getPackageName(), 168 appInfo.user), appInfo -> appInfo.uid, (a, b) -> a)); 169 executeCallbacksTask(c -> c.bindAllApplications(apps, flags, packageUserKeytoUidMap), 170 mUiExecutor); 171 } 172 173 /** 174 * bindWidgets is abstract because it is a no-op for the go launcher. 175 */ bindWidgets()176 public void bindWidgets() { 177 if (!WIDGETS_ENABLED) { 178 return; 179 } 180 List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mContext) 181 .build(mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker()); 182 executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor); 183 } 184 185 /** 186 * bindWidgets is abstract because it is a no-op for the go launcher. 187 */ bindSmartspaceWidget()188 public void bindSmartspaceWidget() { 189 if (!WIDGETS_ENABLED) { 190 return; 191 } 192 executeCallbacksTask(c -> c.bindSmartspaceWidget(), mUiExecutor); 193 } 194 195 /** 196 * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right) 197 */ sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems)198 protected void sortWorkspaceItemsSpatially(ArrayList<ItemInfo> workspaceItems) { 199 final int screenCols = mIDP.numColumns; 200 final int screenCellCount = mIDP.numColumns * mIDP.numRows; 201 Collections.sort(workspaceItems, (lhs, rhs) -> { 202 if (lhs.container == rhs.container) { 203 // Within containers, order by their spatial position in that container 204 switch (lhs.container) { 205 case LauncherSettings.Favorites.CONTAINER_DESKTOP: { 206 int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols 207 + lhs.cellX); 208 int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols 209 + rhs.cellX); 210 return Integer.compare(lr, rr); 211 } 212 case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { 213 // We currently use the screen id as the rank 214 return Integer.compare(lhs.screenId, rhs.screenId); 215 } 216 default: 217 if (FeatureFlags.IS_STUDIO_BUILD) { 218 throw new RuntimeException( 219 "Unexpected container type when sorting workspace items."); 220 } 221 return 0; 222 } 223 } else { 224 // Between containers, order by hotseat, desktop 225 return Integer.compare(lhs.container, rhs.container); 226 } 227 }); 228 } 229 executeCallbacksTask(CallbackTask task, Executor executor)230 protected void executeCallbacksTask(CallbackTask task, Executor executor) { 231 executor.execute(() -> { 232 if (mMyBindingId != mBgDataModel.lastBindId) { 233 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind"); 234 return; 235 } 236 for (Callbacks cb : mCallbacksList) { 237 task.execute(cb); 238 } 239 }); 240 } 241 242 /** 243 * Only used in LoaderTask. 244 */ newIdleLock(Object lock)245 public LooperIdleLock newIdleLock(Object lock) { 246 LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper()); 247 // If we are not binding or if the main looper is already idle, there is no reason to wait 248 if (mUiExecutor.getLooper().getQueue().isIdle()) { 249 idleLock.queueIdle(); 250 } 251 return idleLock; 252 } 253 254 private class UnifiedWorkspaceBinder { 255 256 private final Callbacks mCallbacks; 257 258 private final IntSparseArrayMap<ItemInfo> mItemIdMap; 259 private final IntArray mOrderedScreenIds; 260 private final ArrayList<FixedContainerItems> mExtraItems; 261 UnifiedWorkspaceBinder( Callbacks callbacks, IntSparseArrayMap<ItemInfo> itemIdMap, ArrayList<FixedContainerItems> extraItems, IntArray orderedScreenIds)262 UnifiedWorkspaceBinder( 263 Callbacks callbacks, 264 IntSparseArrayMap<ItemInfo> itemIdMap, 265 ArrayList<FixedContainerItems> extraItems, 266 IntArray orderedScreenIds) { 267 mCallbacks = callbacks; 268 mItemIdMap = itemIdMap; 269 mExtraItems = extraItems; 270 mOrderedScreenIds = orderedScreenIds; 271 } 272 bind(boolean isBindSync, int workspaceItemCount)273 private void bind(boolean isBindSync, int workspaceItemCount) { 274 final IntSet currentScreenIds = 275 mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds); 276 Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks); 277 278 // Separate the items that are on the current screen, and all the other remaining items 279 ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>(); 280 ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>(); 281 ArrayList<ItemInfo> currentAppWidgets = new ArrayList<>(); 282 ArrayList<ItemInfo> otherAppWidgets = new ArrayList<>(); 283 284 Predicate<ItemInfo> currentScreenCheck = currentScreenContentFilter(currentScreenIds); 285 mItemIdMap.forEach(item -> { 286 if (currentScreenCheck.test(item)) { 287 (WIDGET_FILTER.test(item) ? currentAppWidgets : currentWorkspaceItems) 288 .add(item); 289 } else if (item.container == CONTAINER_DESKTOP) { 290 (WIDGET_FILTER.test(item) ? otherAppWidgets : otherWorkspaceItems).add(item); 291 } 292 }); 293 sortWorkspaceItemsSpatially(currentWorkspaceItems); 294 sortWorkspaceItemsSpatially(otherWorkspaceItems); 295 296 // Tell the workspace that we're about to start binding items 297 executeCallbacksTask(c -> { 298 c.clearPendingBinds(); 299 c.startBinding(); 300 if (enableSmartspaceRemovalToggle()) { 301 c.setIsFirstPagePinnedItemEnabled( 302 mBgDataModel.isFirstPagePinnedItemEnabled); 303 } 304 }, mUiExecutor); 305 306 // Bind workspace screens 307 executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); 308 309 ItemInflater inflater = mCallbacks.getItemInflater(); 310 311 // Load items on the current page. 312 if (enableWorkspaceInflation() && inflater != null) { 313 inflateAsyncAndBind(currentWorkspaceItems, inflater, mUiExecutor); 314 inflateAsyncAndBind(currentAppWidgets, inflater, mUiExecutor); 315 } else { 316 bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor); 317 bindItemsInChunks(currentAppWidgets, 1, mUiExecutor); 318 } 319 mExtraItems.forEach(item -> 320 executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor)); 321 322 RunnableList pendingTasks = new RunnableList(); 323 Executor pendingExecutor = pendingTasks::add; 324 325 RunnableList onCompleteSignal = new RunnableList(); 326 onCompleteSignal.add(() -> Log.d(TAG, "Calling onCompleteSignal")); 327 328 if (enableWorkspaceInflation() && inflater != null) { 329 Log.d(TAG, "Starting async inflation"); 330 MODEL_EXECUTOR.execute(() -> { 331 inflateAsyncAndBind(otherWorkspaceItems, inflater, pendingExecutor); 332 inflateAsyncAndBind(otherAppWidgets, inflater, pendingExecutor); 333 setupPendingBind(currentScreenIds, pendingExecutor); 334 335 // Wait for the async inflation to complete and then notify the completion 336 // signal on UI thread. 337 MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy); 338 }); 339 } else { 340 Log.d(TAG, "Starting sync inflation"); 341 bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor); 342 bindItemsInChunks(otherAppWidgets, 1, pendingExecutor); 343 setupPendingBind(currentScreenIds, pendingExecutor); 344 onCompleteSignal.executeAllAndDestroy(); 345 } 346 347 executeCallbacksTask(c -> c.onInitialBindComplete(currentScreenIds, pendingTasks, 348 onCompleteSignal, workspaceItemCount, isBindSync), mUiExecutor); 349 } 350 setupPendingBind( IntSet currentScreenIds, Executor pendingExecutor)351 private void setupPendingBind( 352 IntSet currentScreenIds, 353 Executor pendingExecutor) { 354 StringCache cacheClone = mBgDataModel.stringCache.clone(); 355 executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor); 356 357 executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor); 358 pendingExecutor.execute(() -> ItemInstallQueue.INSTANCE.get(mContext) 359 .resumeModelPush(FLAG_LOADER_RUNNING)); 360 } 361 362 /** 363 * Tries to inflate the items asynchronously and bind. Returns true on success or false if 364 * async-binding is not supported in this case. 365 */ inflateAsyncAndBind( List<ItemInfo> items, @NonNull ItemInflater inflater, Executor executor)366 private void inflateAsyncAndBind( 367 List<ItemInfo> items, @NonNull ItemInflater inflater, Executor executor) { 368 if (mMyBindingId != mBgDataModel.lastBindId) { 369 Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation"); 370 return; 371 } 372 373 ModelWriter writer = mModel.getWriter( 374 false /* verifyChanges */, CellPosMapper.DEFAULT, null); 375 List<Pair<ItemInfo, View>> bindItems = items.stream() 376 .map(i -> Pair.create(i, inflater.inflateItem(i, writer, null))) 377 .collect(Collectors.toList()); 378 executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor); 379 } 380 bindItemsInChunks( List<ItemInfo> workspaceItems, int chunkCount, Executor executor)381 private void bindItemsInChunks( 382 List<ItemInfo> workspaceItems, int chunkCount, Executor executor) { 383 // Bind the workspace items 384 int count = workspaceItems.size(); 385 for (int i = 0; i < count; i += chunkCount) { 386 final int start = i; 387 final int chunkSize = (i + chunkCount <= count) ? chunkCount : (count - i); 388 executeCallbacksTask( 389 c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false), 390 executor); 391 } 392 } 393 executeCallbacksTask(CallbackTask task, Executor executor)394 protected void executeCallbacksTask(CallbackTask task, Executor executor) { 395 executor.execute(() -> { 396 if (mMyBindingId != mBgDataModel.lastBindId) { 397 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind"); 398 return; 399 } 400 task.execute(mCallbacks); 401 }); 402 } 403 } 404 405 @AssistedFactory 406 public interface BaseLauncherBinderFactory { createBinder(Callbacks[] callbacks)407 BaseLauncherBinder createBinder(Callbacks[] callbacks); 408 } 409 } 410