• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 package com.android.launcher3.graphics;
17 
18 import static android.app.WallpaperManager.FLAG_SYSTEM;
19 import static android.view.View.MeasureSpec.EXACTLY;
20 import static android.view.View.MeasureSpec.makeMeasureSpec;
21 import static android.view.View.VISIBLE;
22 
23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
24 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
25 import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
26 import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
27 
28 import android.annotation.TargetApi;
29 import android.app.Fragment;
30 import android.app.WallpaperColors;
31 import android.app.WallpaperManager;
32 import android.appwidget.AppWidgetHostView;
33 import android.appwidget.AppWidgetProviderInfo;
34 import android.content.Context;
35 import android.content.ContextWrapper;
36 import android.content.Intent;
37 import android.content.res.TypedArray;
38 import android.graphics.Color;
39 import android.graphics.Rect;
40 import android.graphics.drawable.AdaptiveIconDrawable;
41 import android.graphics.drawable.ColorDrawable;
42 import android.os.Build;
43 import android.os.Handler;
44 import android.os.Looper;
45 import android.os.Process;
46 import android.util.AttributeSet;
47 import android.util.SparseIntArray;
48 import android.view.ContextThemeWrapper;
49 import android.view.LayoutInflater;
50 import android.view.MotionEvent;
51 import android.view.View;
52 import android.view.ViewGroup;
53 import android.view.WindowInsets;
54 import android.view.WindowManager;
55 import android.widget.TextClock;
56 
57 import com.android.launcher3.BubbleTextView;
58 import com.android.launcher3.CellLayout;
59 import com.android.launcher3.DeviceProfile;
60 import com.android.launcher3.Hotseat;
61 import com.android.launcher3.InsettableFrameLayout;
62 import com.android.launcher3.InvariantDeviceProfile;
63 import com.android.launcher3.LauncherAppState;
64 import com.android.launcher3.LauncherSettings.Favorites;
65 import com.android.launcher3.R;
66 import com.android.launcher3.Utilities;
67 import com.android.launcher3.WorkspaceLayoutManager;
68 import com.android.launcher3.config.FeatureFlags;
69 import com.android.launcher3.folder.FolderIcon;
70 import com.android.launcher3.icons.BaseIconFactory;
71 import com.android.launcher3.icons.BitmapInfo;
72 import com.android.launcher3.icons.LauncherIcons;
73 import com.android.launcher3.model.BgDataModel;
74 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
75 import com.android.launcher3.model.WidgetItem;
76 import com.android.launcher3.model.WidgetsModel;
77 import com.android.launcher3.model.data.FolderInfo;
78 import com.android.launcher3.model.data.ItemInfo;
79 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
80 import com.android.launcher3.model.data.WorkspaceItemInfo;
81 import com.android.launcher3.pm.InstallSessionHelper;
82 import com.android.launcher3.pm.UserCache;
83 import com.android.launcher3.uioverrides.PredictedAppIconInflater;
84 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
85 import com.android.launcher3.util.ComponentKey;
86 import com.android.launcher3.util.IntArray;
87 import com.android.launcher3.util.MainThreadInitializedObject;
88 import com.android.launcher3.views.ActivityContext;
89 import com.android.launcher3.views.BaseDragLayer;
90 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
91 import com.android.launcher3.widget.LocalColorExtractor;
92 import com.android.launcher3.widget.custom.CustomWidgetManager;
93 
94 import java.util.ArrayList;
95 import java.util.Arrays;
96 import java.util.Collections;
97 import java.util.HashMap;
98 import java.util.HashSet;
99 import java.util.List;
100 import java.util.Map;
101 import java.util.Set;
102 import java.util.concurrent.ConcurrentLinkedQueue;
103 
104 /**
105  * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
106  * Steps:
107  *   1) Create a dummy icon info with just white icon
108  *   2) Inflate a strip down layout definition for Launcher
109  *   3) Place appropriate elements like icons and first-page qsb
110  *   4) Measure and draw the view on a canvas
111  */
112 @TargetApi(Build.VERSION_CODES.O)
113 public class LauncherPreviewRenderer extends ContextWrapper
114         implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {
115 
116     /**
117      * Context used just for preview. It also provides a few objects (e.g. UserCache) just for
118      * preview purposes.
119      */
120     public static class PreviewContext extends ContextWrapper {
121 
122         private final Set<MainThreadInitializedObject> mAllowedObjects = new HashSet<>(
123                 Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
124                         LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
125                         CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE));
126 
127         private final InvariantDeviceProfile mIdp;
128         private final Map<MainThreadInitializedObject, Object> mObjectMap = new HashMap<>();
129         private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
130                 new ConcurrentLinkedQueue<>();
131 
132         private boolean mDestroyed = false;
133 
PreviewContext(Context base, InvariantDeviceProfile idp)134         public PreviewContext(Context base, InvariantDeviceProfile idp) {
135             super(base);
136             mIdp = idp;
137             mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp);
138             mObjectMap.put(LauncherAppState.INSTANCE,
139                     new LauncherAppState(this, null /* iconCacheFileName */));
140 
141         }
142 
143         @Override
getApplicationContext()144         public Context getApplicationContext() {
145             return this;
146         }
147 
onDestroy()148         public void onDestroy() {
149             CustomWidgetManager.INSTANCE.get(this).onDestroy();
150             LauncherAppState.INSTANCE.get(this).onTerminate();
151             mDestroyed = true;
152         }
153 
154         /**
155          * Find a cached object from mObjectMap if we have already created one. If not, generate
156          * an object using the provider.
157          */
getObject(MainThreadInitializedObject<T> mainThreadInitializedObject, MainThreadInitializedObject.ObjectProvider<T> provider)158         public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
159                 MainThreadInitializedObject.ObjectProvider<T> provider) {
160             if (FeatureFlags.IS_STUDIO_BUILD && mDestroyed) {
161                 throw new RuntimeException("Context already destroyed");
162             }
163             if (!mAllowedObjects.contains(mainThreadInitializedObject)) {
164                 throw new IllegalStateException("Leaking unknown objects");
165             }
166             if (mObjectMap.containsKey(mainThreadInitializedObject)) {
167                 return (T) mObjectMap.get(mainThreadInitializedObject);
168             }
169             T t = provider.get(this);
170             mObjectMap.put(mainThreadInitializedObject, t);
171             return t;
172         }
173 
newLauncherIcons(Context context, boolean shapeDetection)174         public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) {
175             LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll();
176             if (launcherIconsForPreview != null) {
177                 return launcherIconsForPreview;
178             }
179             return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize,
180                     -1 /* poolId */, shapeDetection);
181         }
182 
183         private final class LauncherIconsForPreview extends LauncherIcons {
184 
LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize, int poolId, boolean shapeDetection)185             private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize,
186                     int poolId, boolean shapeDetection) {
187                 super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection);
188             }
189 
190             @Override
recycle()191             public void recycle() {
192                 // Clear any temporary state variables
193                 clear();
194                 mIconPool.offer(this);
195             }
196         }
197     }
198 
199     private final Handler mUiHandler;
200     private final Context mContext;
201     private final InvariantDeviceProfile mIdp;
202     private final DeviceProfile mDp;
203     private final Rect mInsets;
204     private final WorkspaceItemInfo mWorkspaceItemInfo;
205     private final LayoutInflater mHomeElementInflater;
206     private final InsettableFrameLayout mRootView;
207     private final Hotseat mHotseat;
208     private final CellLayout mWorkspace;
209     private final SparseIntArray mWallpaperColorResources;
210 
LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, WallpaperColors wallpaperColorsOverride)211     public LauncherPreviewRenderer(Context context,
212             InvariantDeviceProfile idp,
213             WallpaperColors wallpaperColorsOverride) {
214 
215         super(context);
216         mUiHandler = new Handler(Looper.getMainLooper());
217         mContext = context;
218         mIdp = idp;
219         mDp = idp.getDeviceProfile(context).copy(context);
220 
221         if (Utilities.ATLEAST_R) {
222             WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class)
223                     .getCurrentWindowMetrics().getWindowInsets();
224             mInsets = new Rect(
225                     currentWindowInsets.getSystemWindowInsetLeft(),
226                     currentWindowInsets.getSystemWindowInsetTop(),
227                     currentWindowInsets.getSystemWindowInsetRight(),
228                     currentWindowInsets.getSystemWindowInsetBottom());
229         } else {
230             mInsets = new Rect();
231             mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2;
232             mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2;
233         }
234         mDp.updateInsets(mInsets);
235 
236         BaseIconFactory iconFactory =
237                 new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { };
238         BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(new AdaptiveIconDrawable(
239                         new ColorDrawable(Color.WHITE), new ColorDrawable(Color.WHITE)),
240                 Process.myUserHandle(),
241                 Build.VERSION.SDK_INT);
242 
243         mWorkspaceItemInfo = new WorkspaceItemInfo();
244         mWorkspaceItemInfo.bitmap = iconInfo;
245         mWorkspaceItemInfo.intent = new Intent();
246         mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
247                 context.getString(R.string.label_application);
248 
249         mHomeElementInflater = LayoutInflater.from(
250                 new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
251         mHomeElementInflater.setFactory2(this);
252 
253         mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
254                 R.layout.launcher_preview_layout, null, false);
255         mRootView.setInsets(mInsets);
256         measureView(mRootView, mDp.widthPx, mDp.heightPx);
257 
258         mHotseat = mRootView.findViewById(R.id.hotseat);
259         mHotseat.resetLayout(false);
260 
261         mWorkspace = mRootView.findViewById(R.id.workspace);
262         mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
263                 mDp.workspacePadding.top,
264                 mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
265                 mDp.workspacePadding.bottom);
266 
267         if (Utilities.ATLEAST_S) {
268             WallpaperColors wallpaperColors = wallpaperColorsOverride != null
269                     ? wallpaperColorsOverride
270                     : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
271             mWallpaperColorResources = wallpaperColors != null ? LocalColorExtractor.newInstance(
272                     context).generateColorsOverride(wallpaperColors) : null;
273         } else {
274             mWallpaperColorResources = null;
275         }
276     }
277 
278     /** Populate preview and render it. */
getRenderedView(BgDataModel dataModel, Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap)279     public View getRenderedView(BgDataModel dataModel,
280             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
281         populate(dataModel, widgetProviderInfoMap);
282         return mRootView;
283     }
284 
285     @Override
onCreateView(View parent, String name, Context context, AttributeSet attrs)286     public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
287         if ("TextClock".equals(name)) {
288             // Workaround for TextClock accessing handler for unregistering ticker.
289             return new TextClock(context, attrs) {
290 
291                 @Override
292                 public Handler getHandler() {
293                     return mUiHandler;
294                 }
295             };
296         } else if (!"fragment".equals(name)) {
297             return null;
298         }
299 
300         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment);
301         FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate(
302                 context, ta.getString(R.styleable.PreviewFragment_android_name));
303         f.enterPreviewMode(context);
304         f.onInit(null);
305 
306         View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null);
307         view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID));
308         return view;
309     }
310 
311     @Override
312     public View onCreateView(String name, Context context, AttributeSet attrs) {
313         return onCreateView(null, name, context, attrs);
314     }
315 
316     @Override
317     public BaseDragLayer getDragLayer() {
318         throw new UnsupportedOperationException();
319     }
320 
321     @Override
322     public DeviceProfile getDeviceProfile() {
323         return mDp;
324     }
325 
326     @Override
327     public Hotseat getHotseat() {
328         return mHotseat;
329     }
330 
331     @Override
332     public CellLayout getScreenWithId(int screenId) {
333         return mWorkspace;
334     }
335 
336     private void inflateAndAddIcon(WorkspaceItemInfo info) {
337         BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
338                 R.layout.app_icon, mWorkspace, false);
339         icon.applyFromWorkspaceItem(info);
340         addInScreenFromBind(icon, info);
341     }
342 
343     private void inflateAndAddFolder(FolderInfo info) {
344         FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
345                 info);
346         addInScreenFromBind(folderIcon, info);
347     }
348 
349     private void inflateAndAddWidgets(
350             LauncherAppWidgetInfo info,
351             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
352         if (widgetProviderInfoMap == null) {
353             return;
354         }
355         AppWidgetProviderInfo providerInfo = widgetProviderInfoMap.get(
356                 new ComponentKey(info.providerName, info.user));
357         if (providerInfo == null) {
358             return;
359         }
360         inflateAndAddWidgets(info, LauncherAppWidgetProviderInfo.fromProviderInfo(
361                 getApplicationContext(), providerInfo));
362     }
363 
364     private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
365         WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
366                 info.providerName);
367         if (widgetItem == null) {
368             return;
369         }
370         inflateAndAddWidgets(info, widgetItem.widgetInfo);
371     }
372 
373     private void inflateAndAddWidgets(
374             LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
375         AppWidgetHostView view = new AppWidgetHostView(mContext);
376         view.setAppWidget(-1, providerInfo);
377         view.updateAppWidget(null);
378         view.setTag(info);
379 
380         if (mWallpaperColorResources != null) {
381             view.setColorResources(mWallpaperColorResources);
382         }
383 
384         addInScreenFromBind(view, info);
385     }
386 
387     private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
388         View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info);
389         if (view != null) {
390             addInScreenFromBind(view, info);
391         }
392     }
393 
394     private void dispatchVisibilityAggregated(View view, boolean isVisible) {
395         // Similar to View.dispatchVisibilityAggregated implementation.
396         final boolean thisVisible = view.getVisibility() == VISIBLE;
397         if (thisVisible || !isVisible) {
398             view.onVisibilityAggregated(isVisible);
399         }
400 
401         if (view instanceof ViewGroup) {
402             isVisible = thisVisible && isVisible;
403             ViewGroup vg = (ViewGroup) view;
404             int count = vg.getChildCount();
405 
406             for (int i = 0; i < count; i++) {
407                 dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
408             }
409         }
410     }
411 
412     private void populate(BgDataModel dataModel,
413             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
414         // Separate the items that are on the current screen, and the other remaining items.
415         ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
416         ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
417         ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
418         ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
419         filterCurrentWorkspaceItems(0 /* currentScreenId */,
420                 dataModel.workspaceItems, currentWorkspaceItems,
421                 otherWorkspaceItems);
422         filterCurrentWorkspaceItems(0 /* currentScreenId */, dataModel.appWidgets,
423                 currentAppWidgets, otherAppWidgets);
424         sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
425         for (ItemInfo itemInfo : currentWorkspaceItems) {
426             switch (itemInfo.itemType) {
427                 case Favorites.ITEM_TYPE_APPLICATION:
428                 case Favorites.ITEM_TYPE_SHORTCUT:
429                 case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
430                     inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
431                     break;
432                 case Favorites.ITEM_TYPE_FOLDER:
433                     inflateAndAddFolder((FolderInfo) itemInfo);
434                     break;
435                 default:
436                     break;
437             }
438         }
439         for (ItemInfo itemInfo : currentAppWidgets) {
440             switch (itemInfo.itemType) {
441                 case Favorites.ITEM_TYPE_APPWIDGET:
442                 case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
443                     if (widgetProviderInfoMap != null) {
444                         inflateAndAddWidgets(
445                                 (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap);
446                     } else {
447                         inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
448                                 dataModel.widgetsModel);
449                     }
450                     break;
451                 default:
452                     break;
453             }
454         }
455         IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
456                 mDp.numShownHotseatIcons);
457         FixedContainerItems hotseatpredictions =
458                 dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
459         List<ItemInfo> predictions = hotseatpredictions == null
460                 ? Collections.emptyList() : hotseatpredictions.items;
461         int count = Math.min(ranks.size(), predictions.size());
462         for (int i = 0; i < count; i++) {
463             int rank = ranks.get(i);
464             WorkspaceItemInfo itemInfo =
465                     new WorkspaceItemInfo((WorkspaceItemInfo) predictions.get(i));
466             itemInfo.container = CONTAINER_HOTSEAT_PREDICTION;
467             itemInfo.rank = rank;
468             itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
469             itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
470             itemInfo.screenId = rank;
471             inflateAndAddPredictedIcon(itemInfo);
472         }
473 
474         // Add first page QSB
475         if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
476             View qsb = mHomeElementInflater.inflate(
477                     R.layout.search_container_workspace, mWorkspace, false);
478             CellLayout.LayoutParams lp =
479                     new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1);
480             lp.canReorder = false;
481             mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
482         }
483 
484         measureView(mRootView, mDp.widthPx, mDp.heightPx);
485         dispatchVisibilityAggregated(mRootView, true);
486         measureView(mRootView, mDp.widthPx, mDp.heightPx);
487         // Additional measure for views which use auto text size API
488         measureView(mRootView, mDp.widthPx, mDp.heightPx);
489     }
490 
491     private static void measureView(View view, int width, int height) {
492         view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
493         view.layout(0, 0, width, height);
494     }
495 
496     /** Root layout for launcher preview that intercepts all touch events. */
497     public static class LauncherPreviewLayout extends InsettableFrameLayout {
498         public LauncherPreviewLayout(Context context, AttributeSet attrs) {
499             super(context, attrs);
500         }
501 
502         @Override
503         public boolean onInterceptTouchEvent(MotionEvent ev) {
504             return true;
505         }
506     }
507 }
508