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