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