• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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