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