• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.graphics;
18 
19 import static android.content.res.Configuration.UI_MODE_NIGHT_NO;
20 import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
21 import static android.view.Display.DEFAULT_DISPLAY;
22 
23 import static com.android.launcher3.Flags.extendibleThemeManager;
24 import static com.android.launcher3.LauncherPrefs.GRID_NAME;
25 import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
26 import static com.android.launcher3.WorkspaceLayoutManager.SECOND_SCREEN_ID;
27 import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
28 import static com.android.launcher3.provider.LauncherDbUtils.selectionForWorkspaceScreen;
29 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
30 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
31 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
32 
33 import android.app.WallpaperColors;
34 import android.appwidget.AppWidgetHost;
35 import android.appwidget.AppWidgetProviderInfo;
36 import android.content.Context;
37 import android.content.res.Configuration;
38 import android.database.Cursor;
39 import android.hardware.display.DisplayManager;
40 import android.os.Bundle;
41 import android.os.IBinder;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import android.util.Size;
45 import android.util.SparseArray;
46 import android.util.SparseIntArray;
47 import android.view.ContextThemeWrapper;
48 import android.view.Display;
49 import android.view.Surface;
50 import android.view.SurfaceControlViewHost;
51 import android.view.SurfaceControlViewHost.SurfacePackage;
52 import android.view.View;
53 import android.view.animation.AccelerateDecelerateInterpolator;
54 import android.widget.FrameLayout;
55 
56 import androidx.annotation.NonNull;
57 import androidx.annotation.Nullable;
58 import androidx.annotation.UiThread;
59 import androidx.annotation.WorkerThread;
60 
61 import com.android.launcher3.DeviceProfile;
62 import com.android.launcher3.InvariantDeviceProfile;
63 import com.android.launcher3.LauncherAppState;
64 import com.android.launcher3.LauncherPrefs;
65 import com.android.launcher3.LauncherSettings;
66 import com.android.launcher3.dagger.LauncherComponentProvider;
67 import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewAppComponent;
68 import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
69 import com.android.launcher3.model.BgDataModel;
70 import com.android.launcher3.model.BgDataModel.Callbacks;
71 import com.android.launcher3.model.LoaderTask;
72 import com.android.launcher3.model.ModelDbController;
73 import com.android.launcher3.model.UserManagerState;
74 import com.android.launcher3.util.ComponentKey;
75 import com.android.launcher3.util.RunnableList;
76 import com.android.launcher3.util.Themes;
77 import com.android.launcher3.widget.LocalColorExtractor;
78 import com.android.systemui.shared.Flags;
79 
80 import java.util.HashMap;
81 import java.util.Map;
82 import java.util.concurrent.TimeUnit;
83 
84 /** Render preview using surface view. */
85 @SuppressWarnings("NewApi")
86 public class PreviewSurfaceRenderer {
87 
88     private static final String TAG = "PreviewSurfaceRenderer";
89     private static final int FADE_IN_ANIMATION_DURATION = 200;
90     private static final String KEY_HOST_TOKEN = "host_token";
91     private static final String KEY_VIEW_WIDTH = "width";
92     private static final String KEY_VIEW_HEIGHT = "height";
93     private static final String KEY_DISPLAY_ID = "display_id";
94     private static final String KEY_COLORS = "wallpaper_colors";
95     private static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids";
96     private static final String KEY_COLOR_VALUES = "color_values";
97     private static final String KEY_DARK_MODE = "use_dark_mode";
98     private static final String KEY_LAYOUT_XML = "layout_xml";
99     public static final String KEY_SKIP_ANIMATIONS = "skip_animations";
100 
101     private final Context mContext;
102     private SparseIntArray mPreviewColorOverride;
103     private String mGridName;
104     private String mShapeKey;
105     private String mLayoutXml;
106 
107     @Nullable private Boolean mDarkMode;
108     private boolean mDestroyed = false;
109     private boolean mHideQsb;
110     @Nullable private FrameLayout mViewRoot = null;
111     private boolean mDeletingHostOnExit = false;
112 
113     private final int mCallingPid;
114     private final IBinder mHostToken;
115     private final int mWidth;
116     private final int mHeight;
117     private final boolean mSkipAnimations;
118     private final int mDisplayId;
119     private final Display mDisplay;
120     private final WallpaperColors mWallpaperColors;
121     private final RunnableList mLifeCycleTracker;
122     private final SurfaceControlViewHost mSurfaceControlViewHost;
123 
PreviewSurfaceRenderer(Context context, RunnableList lifecycleTracker, Bundle bundle, int callingPid)124     public PreviewSurfaceRenderer(Context context, RunnableList lifecycleTracker, Bundle bundle,
125             int callingPid) throws Exception {
126         mContext = context;
127         mLifeCycleTracker = lifecycleTracker;
128         mCallingPid = callingPid;
129         mGridName = bundle.getString("name");
130         bundle.remove("name");
131         if (mGridName == null) {
132             mGridName = LauncherPrefs.get(context).get(GRID_NAME);
133         }
134         mShapeKey = LauncherPrefs.get(context).get(PREF_ICON_SHAPE);
135         mWallpaperColors = bundle.getParcelable(KEY_COLORS);
136         if (Flags.newCustomizationPickerUi()) {
137             updateColorOverrides(bundle);
138         }
139         mHideQsb = bundle.getBoolean(GridCustomizationsProxy.KEY_HIDE_BOTTOM_ROW);
140 
141         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
142         mWidth = bundle.getInt(KEY_VIEW_WIDTH);
143         mHeight = bundle.getInt(KEY_VIEW_HEIGHT);
144         mSkipAnimations = bundle.getBoolean(KEY_SKIP_ANIMATIONS, false);
145         mDisplayId = bundle.getInt(KEY_DISPLAY_ID);
146         mDisplay = context.getSystemService(DisplayManager.class)
147                 .getDisplay(mDisplayId);
148         mLayoutXml = bundle.getString(KEY_LAYOUT_XML);
149         if (mDisplay == null) {
150             throw new IllegalArgumentException("Display ID does not match any displays.");
151         }
152 
153         mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new MySurfaceControlViewHost(
154                         mContext,
155                         context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
156                         mHostToken,
157                         mLifeCycleTracker))
158                 .get(5, TimeUnit.SECONDS);
159         mLifeCycleTracker.add(this::destroy);
160     }
161 
getDisplayId()162     public int getDisplayId() {
163         return mDisplayId;
164     }
165 
getHostToken()166     public IBinder getHostToken() {
167         return mHostToken;
168     }
169 
getSurfacePackage()170     public SurfacePackage getSurfacePackage() {
171         return mSurfaceControlViewHost.getSurfacePackage();
172     }
173 
destroy()174     private void destroy() {
175         mDestroyed = true;
176     }
177 
178     /**
179      * A function that queries for the launcher app widget span info
180      *
181      * @return A SparseArray with the app widget id being the key and the span info being the values
182      */
183     @WorkerThread
184     @Nullable
getLoadedLauncherWidgetInfo()185     public SparseArray<Size> getLoadedLauncherWidgetInfo() {
186         final SparseArray<Size> widgetInfo = new SparseArray<>();
187         final String query = LauncherSettings.Favorites.ITEM_TYPE + " = "
188                 + LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
189 
190         ModelDbController mainController =
191                 LauncherAppState.getInstance(mContext).getModel().getModelDbController();
192         try (Cursor c = mainController.query(
193                 new String[] {
194                         LauncherSettings.Favorites.APPWIDGET_ID,
195                         LauncherSettings.Favorites.SPANX,
196                         LauncherSettings.Favorites.SPANY
197                 }, query, null, null)) {
198             final int appWidgetIdIndex = c.getColumnIndexOrThrow(
199                     LauncherSettings.Favorites.APPWIDGET_ID);
200             final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
201             final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
202             while (c.moveToNext()) {
203                 final int appWidgetId = c.getInt(appWidgetIdIndex);
204                 final int spanX = c.getInt(spanXIndex);
205                 final int spanY = c.getInt(spanYIndex);
206 
207                 widgetInfo.append(appWidgetId, new Size(spanX, spanY));
208             }
209         } catch (Exception e) {
210             Log.e(TAG, "Error querying for launcher widget info", e);
211             return null;
212         }
213 
214         return widgetInfo;
215     }
216 
217     /**
218      * Generates the preview in background
219      */
loadAsync()220     public void loadAsync() {
221         MODEL_EXECUTOR.execute(this::loadModelData);
222     }
223 
224     /**
225      * Update the grid of the launcher preview
226      *
227      * @param gridName Name of the grid, e.g. normal, practical
228      */
updateGrid(@onNull String gridName)229     public void updateGrid(@NonNull String gridName) {
230         if (gridName.equals(mGridName)) {
231             return;
232         }
233         mGridName = gridName;
234         loadAsync();
235     }
236 
237     /**
238      * Update the shapes of the launcher preview
239      *
240      * @param shapeKey key for the IconShape model
241      */
updateShape(String shapeKey)242     public void updateShape(String shapeKey) {
243         if (shapeKey.equals(mShapeKey)) {
244             Log.w(TAG, "Preview shape already set, skipping. shape=" + mShapeKey);
245             return;
246         }
247         mShapeKey = shapeKey;
248         loadAsync();
249     }
250 
251     /**
252      * Hides the components in the bottom row.
253      *
254      * @param hide True to hide and false to show.
255      */
hideBottomRow(boolean hide)256     public void hideBottomRow(boolean hide) {
257         mHideQsb = hide;
258         loadAsync();
259     }
260 
261     /**
262      * Updates the colors of the preview.
263      *
264      * @param bundle Bundle with an int array of color ids and an int array of overriding colors.
265      */
previewColor(Bundle bundle)266     public void previewColor(Bundle bundle) {
267         updateColorOverrides(bundle);
268         loadAsync();
269     }
270 
updateColorOverrides(Bundle bundle)271     private void updateColorOverrides(Bundle bundle) {
272         mDarkMode =
273                 bundle.containsKey(KEY_DARK_MODE) ? bundle.getBoolean(KEY_DARK_MODE) : null;
274         int[] ids = bundle.getIntArray(KEY_COLOR_RESOURCE_IDS);
275         int[] colors = bundle.getIntArray(KEY_COLOR_VALUES);
276         if (ids != null && colors != null) {
277             mPreviewColorOverride = new SparseIntArray();
278             for (int i = 0; i < ids.length; i++) {
279                 mPreviewColorOverride.put(ids[i], colors[i]);
280             }
281         } else {
282             mPreviewColorOverride = null;
283         }
284     }
285 
286     /***
287      * Generates a new context overriding the theme color and the display size without affecting the
288      * main application context
289      */
getPreviewContext()290     private Context getPreviewContext() {
291         Context context = mContext.createDisplayContext(mDisplay);
292         if (mDarkMode != null) {
293             Configuration configuration = new Configuration(
294                     context.getResources().getConfiguration());
295             if (mDarkMode) {
296                 configuration.uiMode &= ~UI_MODE_NIGHT_NO;
297                 configuration.uiMode |= UI_MODE_NIGHT_YES;
298             } else {
299                 configuration.uiMode &= ~UI_MODE_NIGHT_YES;
300                 configuration.uiMode |= UI_MODE_NIGHT_NO;
301             }
302             context = context.createConfigurationContext(configuration);
303         }
304         if (InvariantDeviceProfile.INSTANCE.get(context).isFixedLandscape) {
305             Configuration configuration = new Configuration(
306                     context.getResources().getConfiguration()
307             );
308             int width = configuration.screenWidthDp;
309             int height = configuration.screenHeightDp;
310             if (configuration.screenHeightDp > configuration.screenWidthDp) {
311                 configuration.screenWidthDp = height;
312                 configuration.screenHeightDp = width;
313                 configuration.orientation = Surface.ROTATION_90;
314             }
315             context = context.createConfigurationContext(configuration);
316         }
317 
318         if (Flags.newCustomizationPickerUi()) {
319             if (mPreviewColorOverride != null) {
320                 LocalColorExtractor.newInstance(context)
321                         .applyColorsOverride(context, mPreviewColorOverride);
322             } else if (mWallpaperColors != null) {
323                 LocalColorExtractor.newInstance(context)
324                         .applyColorsOverride(context, mWallpaperColors);
325             }
326             if (mWallpaperColors != null) {
327                 return new ContextThemeWrapper(context,
328                         Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
329             } else {
330                 return new ContextThemeWrapper(context,
331                         Themes.getActivityThemeRes(context));
332             }
333         } else {
334             if (mWallpaperColors == null) {
335                 return new ContextThemeWrapper(context,
336                         Themes.getActivityThemeRes(context));
337             }
338             LocalColorExtractor.newInstance(context)
339                     .applyColorsOverride(context, mWallpaperColors);
340             return new ContextThemeWrapper(context,
341                     Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
342         }
343     }
344 
345     @WorkerThread
loadModelData()346     private void loadModelData() {
347         final Context inflationContext = getPreviewContext();
348         if (!mGridName.equals(LauncherPrefs.INSTANCE.get(mContext).get(GRID_NAME))
349                 || !mShapeKey.equals(LauncherPrefs.INSTANCE.get(mContext).get(PREF_ICON_SHAPE))
350                 || !TextUtils.isEmpty(mLayoutXml)) {
351 
352             boolean isCustomLayout = extendibleThemeManager() &&  !TextUtils.isEmpty(mLayoutXml);
353             int widgetHostId = isCustomLayout ? APPWIDGET_HOST_ID + mCallingPid : APPWIDGET_HOST_ID;
354 
355             // Start the migration
356             PreviewContext previewContext = new PreviewContext(
357                     inflationContext, mGridName, mShapeKey, widgetHostId, mLayoutXml);
358             PreviewAppComponent appComponent =
359                     (PreviewAppComponent) LauncherComponentProvider.get(previewContext);
360 
361             if (extendibleThemeManager() && isCustomLayout && !mDeletingHostOnExit) {
362                 mDeletingHostOnExit = true;
363                 mLifeCycleTracker.add(() -> {
364                     AppWidgetHost host = new AppWidgetHost(mContext, widgetHostId);
365                     // Start listening here, so that any previous active host is disabled
366                     host.startListening();
367                     host.stopListening();
368                     host.deleteHost();
369                 });
370             }
371 
372             LoaderTask task = appComponent.getLoaderTaskFactory().newLoaderTask(
373                     appComponent.getBaseLauncherBinderFactory().createBinder(new Callbacks[0]),
374                     new UserManagerState());
375 
376             InvariantDeviceProfile idp = appComponent.getIDP();
377             DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
378             String query = deviceProfile.isTwoPanels
379                     ? selectionForWorkspaceScreen(FIRST_SCREEN_ID, SECOND_SCREEN_ID)
380                     : selectionForWorkspaceScreen(FIRST_SCREEN_ID);
381             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap = new HashMap<>();
382             task.loadWorkspaceForPreview(query, widgetProviderInfoMap);
383             final SparseArray<Size> spanInfo = getLoadedLauncherWidgetInfo();
384             MAIN_EXECUTOR.execute(() -> {
385                 renderView(previewContext, appComponent.getDataModel(), widgetHostId,
386                         widgetProviderInfoMap, spanInfo, idp);
387                 mLifeCycleTracker.add(previewContext::onDestroy);
388             });
389         } else {
390             LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
391                 if (dataModel != null) {
392                     MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel,
393                             APPWIDGET_HOST_ID, null, null,
394                             LauncherAppState.getIDP(inflationContext)));
395                 } else {
396                     Log.e(TAG, "Model loading failed");
397                 }
398             });
399         }
400     }
401 
402     @UiThread
renderView(Context inflationContext, BgDataModel dataModel, int widgetHostId, Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap, @Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp)403     private void renderView(Context inflationContext, BgDataModel dataModel, int widgetHostId,
404             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
405             @Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp) {
406         if (mDestroyed) {
407             return;
408         }
409         LauncherPreviewRenderer renderer;
410         if (Flags.newCustomizationPickerUi()) {
411             renderer = new LauncherPreviewRenderer(inflationContext, idp, widgetHostId,
412                     mPreviewColorOverride, mWallpaperColors, launcherWidgetSpanInfo);
413         } else {
414             renderer = new LauncherPreviewRenderer(inflationContext, idp, widgetHostId,
415                     mWallpaperColors, launcherWidgetSpanInfo);
416         }
417         renderer.hideBottomRow(mHideQsb);
418         View view = renderer.getRenderedView(dataModel, widgetProviderInfoMap);
419 
420         view.setPivotX(0);
421         view.setPivotY(0);
422         if (idp.isFixedLandscape) {
423             final float scale = Math.min(mHeight / (float) view.getMeasuredWidth(),
424                     mWidth / (float) view.getMeasuredHeight());
425             view.setScaleX(scale);
426             view.setScaleY(scale);
427             view.setRotation(90);
428             view.setTranslationX((mHeight - scale * view.getWidth()) / 2 + mWidth);
429             view.setTranslationY((mWidth - scale * view.getHeight()) / 2);
430         } else {
431             // This aspect scales the view to fit in the surface and centers it
432             final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
433                     mHeight / (float) view.getMeasuredHeight());
434             view.setScaleX(scale);
435             view.setScaleY(scale);
436             view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
437             view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
438         }
439 
440         if (!Flags.newCustomizationPickerUi()) {
441             view.setAlpha(mSkipAnimations ? 1 : 0);
442             view.animate().alpha(1)
443                     .setInterpolator(new AccelerateDecelerateInterpolator())
444                     .setDuration(FADE_IN_ANIMATION_DURATION)
445                     .start();
446             mSurfaceControlViewHost.setView(
447                     view,
448                     view.getMeasuredWidth(),
449                     view.getMeasuredHeight()
450             );
451             return;
452         }
453 
454         if (mViewRoot == null) {
455             mViewRoot = new FrameLayout(inflationContext);
456             FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
457                     FrameLayout.LayoutParams.WRAP_CONTENT, // Width
458                     FrameLayout.LayoutParams.WRAP_CONTENT  // Height
459             );
460             mViewRoot.setLayoutParams(layoutParams);
461             mViewRoot.addView(view);
462             mViewRoot.setAlpha(mSkipAnimations ? 1 : 0);
463             mViewRoot.animate().alpha(1)
464                     .setInterpolator(new AccelerateDecelerateInterpolator())
465                     .setDuration(FADE_IN_ANIMATION_DURATION)
466                     .start();
467             mSurfaceControlViewHost.setView(
468                     mViewRoot,
469                     view.getMeasuredWidth(),
470                     view.getMeasuredHeight()
471             );
472         } else  {
473             mViewRoot.removeAllViews();
474             mViewRoot.addView(view);
475         }
476     }
477 
478     private static class MySurfaceControlViewHost extends SurfaceControlViewHost {
479 
480         private final RunnableList mLifecycleTracker;
481 
MySurfaceControlViewHost(Context context, Display display, IBinder hostToken, RunnableList lifeCycleTracker)482         MySurfaceControlViewHost(Context context, Display display, IBinder hostToken,
483                 RunnableList lifeCycleTracker) {
484             super(context, display, hostToken);
485             mLifecycleTracker = lifeCycleTracker;
486             mLifecycleTracker.add(this::release);
487         }
488 
489         @Override
release()490         public void release() {
491             super.release();
492             // RunnableList ensures that the callback is only called once
493             MAIN_EXECUTOR.execute(mLifecycleTracker::executeAllAndDestroy);
494         }
495     }
496 }
497