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