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.view.Display.DEFAULT_DISPLAY;
20 
21 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
23 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
24 
25 import android.app.WallpaperColors;
26 import android.appwidget.AppWidgetProviderInfo;
27 import android.content.Context;
28 import android.database.Cursor;
29 import android.hardware.display.DisplayManager;
30 import android.os.Bundle;
31 import android.os.IBinder;
32 import android.util.Log;
33 import android.util.Size;
34 import android.util.SparseArray;
35 import android.view.ContextThemeWrapper;
36 import android.view.Display;
37 import android.view.SurfaceControlViewHost;
38 import android.view.SurfaceControlViewHost.SurfacePackage;
39 import android.view.View;
40 import android.view.animation.AccelerateDecelerateInterpolator;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.UiThread;
45 import androidx.annotation.WorkerThread;
46 
47 import com.android.launcher3.DeviceProfile;
48 import com.android.launcher3.InvariantDeviceProfile;
49 import com.android.launcher3.LauncherAppState;
50 import com.android.launcher3.LauncherSettings;
51 import com.android.launcher3.Workspace;
52 import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
53 import com.android.launcher3.model.BaseLauncherBinder;
54 import com.android.launcher3.model.BgDataModel;
55 import com.android.launcher3.model.BgDataModel.Callbacks;
56 import com.android.launcher3.model.GridSizeMigrationUtil;
57 import com.android.launcher3.model.LoaderTask;
58 import com.android.launcher3.model.ModelDbController;
59 import com.android.launcher3.provider.LauncherDbUtils;
60 import com.android.launcher3.util.ComponentKey;
61 import com.android.launcher3.util.RunnableList;
62 import com.android.launcher3.util.Themes;
63 import com.android.launcher3.widget.LocalColorExtractor;
64 
65 import java.util.ArrayList;
66 import java.util.Map;
67 import java.util.concurrent.TimeUnit;
68 
69 /** Render preview using surface view. */
70 @SuppressWarnings("NewApi")
71 public class PreviewSurfaceRenderer {
72 
73     private static final String TAG = "PreviewSurfaceRenderer";
74 
75     private static final int FADE_IN_ANIMATION_DURATION = 200;
76 
77     private static final String KEY_HOST_TOKEN = "host_token";
78     private static final String KEY_VIEW_WIDTH = "width";
79     private static final String KEY_VIEW_HEIGHT = "height";
80     private static final String KEY_DISPLAY_ID = "display_id";
81     private static final String KEY_COLORS = "wallpaper_colors";
82 
83     private Context mContext;
84     private final IBinder mHostToken;
85     private final int mWidth;
86     private final int mHeight;
87     private String mGridName;
88 
89     private final int mDisplayId;
90     private final Display mDisplay;
91     private final WallpaperColors mWallpaperColors;
92     private final RunnableList mOnDestroyCallbacks = new RunnableList();
93 
94     private final SurfaceControlViewHost mSurfaceControlViewHost;
95 
96     private boolean mDestroyed = false;
97     private LauncherPreviewRenderer mRenderer;
98     private boolean mHideQsb;
99 
PreviewSurfaceRenderer(Context context, Bundle bundle)100     public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
101         mContext = context;
102         mGridName = bundle.getString("name");
103         bundle.remove("name");
104         if (mGridName == null) {
105             mGridName = InvariantDeviceProfile.getCurrentGridName(context);
106         }
107         mWallpaperColors = bundle.getParcelable(KEY_COLORS);
108         mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
109 
110         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
111         mWidth = bundle.getInt(KEY_VIEW_WIDTH);
112         mHeight = bundle.getInt(KEY_VIEW_HEIGHT);
113         mDisplayId = bundle.getInt(KEY_DISPLAY_ID);
114         mDisplay = context.getSystemService(DisplayManager.class)
115                 .getDisplay(mDisplayId);
116         if (mDisplay == null) {
117             throw new IllegalArgumentException("Display ID does not match any displays.");
118         }
119 
120         mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() ->
121                 new SurfaceControlViewHost(mContext, context.getSystemService(DisplayManager.class)
122                         .getDisplay(DEFAULT_DISPLAY), mHostToken)
123         ).get(5, TimeUnit.SECONDS);
124         mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
125     }
126 
getDisplayId()127     public int getDisplayId() {
128         return mDisplayId;
129     }
130 
getHostToken()131     public IBinder getHostToken() {
132         return mHostToken;
133     }
134 
getSurfacePackage()135     public SurfacePackage getSurfacePackage() {
136         return mSurfaceControlViewHost.getSurfacePackage();
137     }
138 
139     /**
140      * Destroys the preview and all associated data
141      */
142     @UiThread
destroy()143     public void destroy() {
144         mDestroyed = true;
145         mOnDestroyCallbacks.executeAllAndDestroy();
146     }
147 
148     /**
149      * A function that queries for the launcher app widget span info
150      *
151      * @param context The context to get the content resolver from, should be related to launcher
152      * @return A SparseArray with the app widget id being the key and the span info being the values
153      */
154     @WorkerThread
155     @Nullable
getLoadedLauncherWidgetInfo( @onNull final Context context)156     public SparseArray<Size> getLoadedLauncherWidgetInfo(
157             @NonNull final Context context) {
158         final SparseArray<Size> widgetInfo = new SparseArray<>();
159         final String query = LauncherSettings.Favorites.ITEM_TYPE + " = "
160                 + LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
161 
162         ModelDbController mainController =
163                 LauncherAppState.getInstance(mContext).getModel().getModelDbController();
164         try (Cursor c = mainController.query(TABLE_NAME,
165                 new String[] {
166                         LauncherSettings.Favorites.APPWIDGET_ID,
167                         LauncherSettings.Favorites.SPANX,
168                         LauncherSettings.Favorites.SPANY
169                 }, query, null, null)) {
170             final int appWidgetIdIndex = c.getColumnIndexOrThrow(
171                     LauncherSettings.Favorites.APPWIDGET_ID);
172             final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
173             final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
174             while (c.moveToNext()) {
175                 final int appWidgetId = c.getInt(appWidgetIdIndex);
176                 final int spanX = c.getInt(spanXIndex);
177                 final int spanY = c.getInt(spanYIndex);
178 
179                 widgetInfo.append(appWidgetId, new Size(spanX, spanY));
180             }
181         } catch (Exception e) {
182             Log.e(TAG, "Error querying for launcher widget info", e);
183             return null;
184         }
185 
186         return widgetInfo;
187     }
188 
189     /**
190      * Generates the preview in background
191      */
loadAsync()192     public void loadAsync() {
193         MODEL_EXECUTOR.execute(this::loadModelData);
194     }
195 
196     /**
197      * Hides the components in the bottom row.
198      *
199      * @param hide True to hide and false to show.
200      */
hideBottomRow(boolean hide)201     public void hideBottomRow(boolean hide) {
202         if (mRenderer != null) {
203             mRenderer.hideBottomRow(hide);
204         }
205     }
206 
207     /***
208      * Generates a new context overriding the theme color and the display size without affecting the
209      * main application context
210      */
getPreviewContext()211     private Context getPreviewContext() {
212         Context context = mContext.createDisplayContext(mDisplay);
213         if (mWallpaperColors == null) {
214             return new ContextThemeWrapper(context,
215                     Themes.getActivityThemeRes(context));
216         }
217         LocalColorExtractor.newInstance(context)
218                 .applyColorsOverride(context, mWallpaperColors);
219         return new ContextThemeWrapper(context,
220                 Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
221     }
222 
223     @WorkerThread
loadModelData()224     private void loadModelData() {
225         final Context inflationContext = getPreviewContext();
226         final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
227         if (GridSizeMigrationUtil.needsToMigrate(inflationContext, idp)) {
228             // Start the migration
229             PreviewContext previewContext = new PreviewContext(inflationContext, idp);
230             // Copy existing data to preview DB
231             LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext)
232                             .getModel().getModelDbController().getDb(),
233                     TABLE_NAME,
234                     LauncherAppState.getInstance(previewContext)
235                             .getModel().getModelDbController().getDb(),
236                     TABLE_NAME,
237                     mContext);
238             LauncherAppState.getInstance(previewContext)
239                     .getModel().getModelDbController().clearEmptyDbFlag();
240 
241             BgDataModel bgModel = new BgDataModel();
242             new LoaderTask(
243                     LauncherAppState.getInstance(previewContext),
244                     /* bgAllAppsList= */ null,
245                     bgModel,
246                     LauncherAppState.getInstance(previewContext).getModel().getModelDelegate(),
247                     new BaseLauncherBinder(LauncherAppState.getInstance(previewContext), bgModel,
248                             /* bgAllAppsList= */ null, new Callbacks[0])) {
249 
250                 @Override
251                 public void run() {
252                     DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
253                     String query =
254                             LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
255                                     + " or " + LauncherSettings.Favorites.CONTAINER + " = "
256                                     + LauncherSettings.Favorites.CONTAINER_HOTSEAT;
257                     if (deviceProfile.isTwoPanels) {
258                         query += " or " + LauncherSettings.Favorites.SCREEN + " = "
259                                 + Workspace.SECOND_SCREEN_ID;
260                     }
261                     loadWorkspace(new ArrayList<>(), query, null, null);
262 
263                     final SparseArray<Size> spanInfo =
264                             getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
265 
266                     MAIN_EXECUTOR.execute(() -> {
267                         renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo,
268                                 idp);
269                         mOnDestroyCallbacks.add(previewContext::onDestroy);
270                     });
271                 }
272             }.run();
273         } else {
274             LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
275                 if (dataModel != null) {
276                     MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel, null,
277                             null, idp));
278                 } else {
279                     Log.e(TAG, "Model loading failed");
280                 }
281             });
282         }
283     }
284 
285     @UiThread
renderView(Context inflationContext, BgDataModel dataModel, Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap, @Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp)286     private void renderView(Context inflationContext, BgDataModel dataModel,
287             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
288             @Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp) {
289         if (mDestroyed) {
290             return;
291         }
292         mRenderer = new LauncherPreviewRenderer(inflationContext, idp,
293                 mWallpaperColors, launcherWidgetSpanInfo);
294         mRenderer.hideBottomRow(mHideQsb);
295         View view = mRenderer.getRenderedView(dataModel, widgetProviderInfoMap);
296         // This aspect scales the view to fit in the surface and centers it
297         final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
298                 mHeight / (float) view.getMeasuredHeight());
299         view.setScaleX(scale);
300         view.setScaleY(scale);
301         view.setPivotX(0);
302         view.setPivotY(0);
303         view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
304         view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
305         view.setAlpha(0);
306         view.animate().alpha(1)
307                 .setInterpolator(new AccelerateDecelerateInterpolator())
308                 .setDuration(FADE_IN_ANIMATION_DURATION)
309                 .start();
310         mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());
311     }
312 }
313