1 /* 2 * Copyright (C) 2018 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 package com.android.launcher3.graphics; 17 18 import static android.app.WallpaperManager.FLAG_SYSTEM; 19 import static android.view.View.MeasureSpec.EXACTLY; 20 import static android.view.View.MeasureSpec.makeMeasureSpec; 21 import static android.view.View.VISIBLE; 22 23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 24 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems; 25 import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks; 26 import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially; 27 28 import android.annotation.TargetApi; 29 import android.app.Fragment; 30 import android.app.WallpaperColors; 31 import android.app.WallpaperManager; 32 import android.appwidget.AppWidgetHostView; 33 import android.appwidget.AppWidgetProviderInfo; 34 import android.content.Context; 35 import android.content.ContextWrapper; 36 import android.content.Intent; 37 import android.content.res.TypedArray; 38 import android.graphics.Color; 39 import android.graphics.Rect; 40 import android.graphics.drawable.AdaptiveIconDrawable; 41 import android.graphics.drawable.ColorDrawable; 42 import android.os.Build; 43 import android.os.Handler; 44 import android.os.Looper; 45 import android.os.Process; 46 import android.util.AttributeSet; 47 import android.util.SparseIntArray; 48 import android.view.ContextThemeWrapper; 49 import android.view.LayoutInflater; 50 import android.view.MotionEvent; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.view.WindowInsets; 54 import android.view.WindowManager; 55 import android.widget.TextClock; 56 57 import com.android.launcher3.BubbleTextView; 58 import com.android.launcher3.CellLayout; 59 import com.android.launcher3.DeviceProfile; 60 import com.android.launcher3.Hotseat; 61 import com.android.launcher3.InsettableFrameLayout; 62 import com.android.launcher3.InvariantDeviceProfile; 63 import com.android.launcher3.LauncherAppState; 64 import com.android.launcher3.LauncherSettings.Favorites; 65 import com.android.launcher3.R; 66 import com.android.launcher3.Utilities; 67 import com.android.launcher3.WorkspaceLayoutManager; 68 import com.android.launcher3.config.FeatureFlags; 69 import com.android.launcher3.folder.FolderIcon; 70 import com.android.launcher3.icons.BaseIconFactory; 71 import com.android.launcher3.icons.BitmapInfo; 72 import com.android.launcher3.icons.LauncherIcons; 73 import com.android.launcher3.model.BgDataModel; 74 import com.android.launcher3.model.BgDataModel.FixedContainerItems; 75 import com.android.launcher3.model.WidgetItem; 76 import com.android.launcher3.model.WidgetsModel; 77 import com.android.launcher3.model.data.FolderInfo; 78 import com.android.launcher3.model.data.ItemInfo; 79 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 80 import com.android.launcher3.model.data.WorkspaceItemInfo; 81 import com.android.launcher3.pm.InstallSessionHelper; 82 import com.android.launcher3.pm.UserCache; 83 import com.android.launcher3.uioverrides.PredictedAppIconInflater; 84 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; 85 import com.android.launcher3.util.ComponentKey; 86 import com.android.launcher3.util.IntArray; 87 import com.android.launcher3.util.MainThreadInitializedObject; 88 import com.android.launcher3.views.ActivityContext; 89 import com.android.launcher3.views.BaseDragLayer; 90 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 91 import com.android.launcher3.widget.LocalColorExtractor; 92 import com.android.launcher3.widget.custom.CustomWidgetManager; 93 94 import java.util.ArrayList; 95 import java.util.Arrays; 96 import java.util.Collections; 97 import java.util.HashMap; 98 import java.util.HashSet; 99 import java.util.List; 100 import java.util.Map; 101 import java.util.Set; 102 import java.util.concurrent.ConcurrentLinkedQueue; 103 104 /** 105 * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile. 106 * Steps: 107 * 1) Create a dummy icon info with just white icon 108 * 2) Inflate a strip down layout definition for Launcher 109 * 3) Place appropriate elements like icons and first-page qsb 110 * 4) Measure and draw the view on a canvas 111 */ 112 @TargetApi(Build.VERSION_CODES.O) 113 public class LauncherPreviewRenderer extends ContextWrapper 114 implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 { 115 116 /** 117 * Context used just for preview. It also provides a few objects (e.g. UserCache) just for 118 * preview purposes. 119 */ 120 public static class PreviewContext extends ContextWrapper { 121 122 private final Set<MainThreadInitializedObject> mAllowedObjects = new HashSet<>( 123 Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE, 124 LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, 125 CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE)); 126 127 private final InvariantDeviceProfile mIdp; 128 private final Map<MainThreadInitializedObject, Object> mObjectMap = new HashMap<>(); 129 private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool = 130 new ConcurrentLinkedQueue<>(); 131 132 private boolean mDestroyed = false; 133 PreviewContext(Context base, InvariantDeviceProfile idp)134 public PreviewContext(Context base, InvariantDeviceProfile idp) { 135 super(base); 136 mIdp = idp; 137 mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp); 138 mObjectMap.put(LauncherAppState.INSTANCE, 139 new LauncherAppState(this, null /* iconCacheFileName */)); 140 141 } 142 143 @Override getApplicationContext()144 public Context getApplicationContext() { 145 return this; 146 } 147 onDestroy()148 public void onDestroy() { 149 CustomWidgetManager.INSTANCE.get(this).onDestroy(); 150 LauncherAppState.INSTANCE.get(this).onTerminate(); 151 mDestroyed = true; 152 } 153 154 /** 155 * Find a cached object from mObjectMap if we have already created one. If not, generate 156 * an object using the provider. 157 */ getObject(MainThreadInitializedObject<T> mainThreadInitializedObject, MainThreadInitializedObject.ObjectProvider<T> provider)158 public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject, 159 MainThreadInitializedObject.ObjectProvider<T> provider) { 160 if (FeatureFlags.IS_STUDIO_BUILD && mDestroyed) { 161 throw new RuntimeException("Context already destroyed"); 162 } 163 if (!mAllowedObjects.contains(mainThreadInitializedObject)) { 164 throw new IllegalStateException("Leaking unknown objects"); 165 } 166 if (mObjectMap.containsKey(mainThreadInitializedObject)) { 167 return (T) mObjectMap.get(mainThreadInitializedObject); 168 } 169 T t = provider.get(this); 170 mObjectMap.put(mainThreadInitializedObject, t); 171 return t; 172 } 173 newLauncherIcons(Context context, boolean shapeDetection)174 public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) { 175 LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll(); 176 if (launcherIconsForPreview != null) { 177 return launcherIconsForPreview; 178 } 179 return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize, 180 -1 /* poolId */, shapeDetection); 181 } 182 183 private final class LauncherIconsForPreview extends LauncherIcons { 184 LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize, int poolId, boolean shapeDetection)185 private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize, 186 int poolId, boolean shapeDetection) { 187 super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection); 188 } 189 190 @Override recycle()191 public void recycle() { 192 // Clear any temporary state variables 193 clear(); 194 mIconPool.offer(this); 195 } 196 } 197 } 198 199 private final Handler mUiHandler; 200 private final Context mContext; 201 private final InvariantDeviceProfile mIdp; 202 private final DeviceProfile mDp; 203 private final Rect mInsets; 204 private final WorkspaceItemInfo mWorkspaceItemInfo; 205 private final LayoutInflater mHomeElementInflater; 206 private final InsettableFrameLayout mRootView; 207 private final Hotseat mHotseat; 208 private final CellLayout mWorkspace; 209 private final SparseIntArray mWallpaperColorResources; 210 LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, WallpaperColors wallpaperColorsOverride)211 public LauncherPreviewRenderer(Context context, 212 InvariantDeviceProfile idp, 213 WallpaperColors wallpaperColorsOverride) { 214 215 super(context); 216 mUiHandler = new Handler(Looper.getMainLooper()); 217 mContext = context; 218 mIdp = idp; 219 mDp = idp.getDeviceProfile(context).copy(context); 220 221 if (Utilities.ATLEAST_R) { 222 WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class) 223 .getCurrentWindowMetrics().getWindowInsets(); 224 mInsets = new Rect( 225 currentWindowInsets.getSystemWindowInsetLeft(), 226 currentWindowInsets.getSystemWindowInsetTop(), 227 currentWindowInsets.getSystemWindowInsetRight(), 228 currentWindowInsets.getSystemWindowInsetBottom()); 229 } else { 230 mInsets = new Rect(); 231 mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2; 232 mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2; 233 } 234 mDp.updateInsets(mInsets); 235 236 BaseIconFactory iconFactory = 237 new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { }; 238 BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(new AdaptiveIconDrawable( 239 new ColorDrawable(Color.WHITE), new ColorDrawable(Color.WHITE)), 240 Process.myUserHandle(), 241 Build.VERSION.SDK_INT); 242 243 mWorkspaceItemInfo = new WorkspaceItemInfo(); 244 mWorkspaceItemInfo.bitmap = iconInfo; 245 mWorkspaceItemInfo.intent = new Intent(); 246 mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title = 247 context.getString(R.string.label_application); 248 249 mHomeElementInflater = LayoutInflater.from( 250 new ContextThemeWrapper(this, R.style.HomeScreenElementTheme)); 251 mHomeElementInflater.setFactory2(this); 252 253 mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate( 254 R.layout.launcher_preview_layout, null, false); 255 mRootView.setInsets(mInsets); 256 measureView(mRootView, mDp.widthPx, mDp.heightPx); 257 258 mHotseat = mRootView.findViewById(R.id.hotseat); 259 mHotseat.resetLayout(false); 260 261 mWorkspace = mRootView.findViewById(R.id.workspace); 262 mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx, 263 mDp.workspacePadding.top, 264 mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx, 265 mDp.workspacePadding.bottom); 266 267 if (Utilities.ATLEAST_S) { 268 WallpaperColors wallpaperColors = wallpaperColorsOverride != null 269 ? wallpaperColorsOverride 270 : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM); 271 mWallpaperColorResources = wallpaperColors != null ? LocalColorExtractor.newInstance( 272 context).generateColorsOverride(wallpaperColors) : null; 273 } else { 274 mWallpaperColorResources = null; 275 } 276 } 277 278 /** Populate preview and render it. */ getRenderedView(BgDataModel dataModel, Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap)279 public View getRenderedView(BgDataModel dataModel, 280 Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) { 281 populate(dataModel, widgetProviderInfoMap); 282 return mRootView; 283 } 284 285 @Override onCreateView(View parent, String name, Context context, AttributeSet attrs)286 public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { 287 if ("TextClock".equals(name)) { 288 // Workaround for TextClock accessing handler for unregistering ticker. 289 return new TextClock(context, attrs) { 290 291 @Override 292 public Handler getHandler() { 293 return mUiHandler; 294 } 295 }; 296 } else if (!"fragment".equals(name)) { 297 return null; 298 } 299 300 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment); 301 FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate( 302 context, ta.getString(R.styleable.PreviewFragment_android_name)); 303 f.enterPreviewMode(context); 304 f.onInit(null); 305 306 View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null); 307 view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID)); 308 return view; 309 } 310 311 @Override 312 public View onCreateView(String name, Context context, AttributeSet attrs) { 313 return onCreateView(null, name, context, attrs); 314 } 315 316 @Override 317 public BaseDragLayer getDragLayer() { 318 throw new UnsupportedOperationException(); 319 } 320 321 @Override 322 public DeviceProfile getDeviceProfile() { 323 return mDp; 324 } 325 326 @Override 327 public Hotseat getHotseat() { 328 return mHotseat; 329 } 330 331 @Override 332 public CellLayout getScreenWithId(int screenId) { 333 return mWorkspace; 334 } 335 336 private void inflateAndAddIcon(WorkspaceItemInfo info) { 337 BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate( 338 R.layout.app_icon, mWorkspace, false); 339 icon.applyFromWorkspaceItem(info); 340 addInScreenFromBind(icon, info); 341 } 342 343 private void inflateAndAddFolder(FolderInfo info) { 344 FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace, 345 info); 346 addInScreenFromBind(folderIcon, info); 347 } 348 349 private void inflateAndAddWidgets( 350 LauncherAppWidgetInfo info, 351 Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) { 352 if (widgetProviderInfoMap == null) { 353 return; 354 } 355 AppWidgetProviderInfo providerInfo = widgetProviderInfoMap.get( 356 new ComponentKey(info.providerName, info.user)); 357 if (providerInfo == null) { 358 return; 359 } 360 inflateAndAddWidgets(info, LauncherAppWidgetProviderInfo.fromProviderInfo( 361 getApplicationContext(), providerInfo)); 362 } 363 364 private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) { 365 WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName( 366 info.providerName); 367 if (widgetItem == null) { 368 return; 369 } 370 inflateAndAddWidgets(info, widgetItem.widgetInfo); 371 } 372 373 private void inflateAndAddWidgets( 374 LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) { 375 AppWidgetHostView view = new AppWidgetHostView(mContext); 376 view.setAppWidget(-1, providerInfo); 377 view.updateAppWidget(null); 378 view.setTag(info); 379 380 if (mWallpaperColorResources != null) { 381 view.setColorResources(mWallpaperColorResources); 382 } 383 384 addInScreenFromBind(view, info); 385 } 386 387 private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) { 388 View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info); 389 if (view != null) { 390 addInScreenFromBind(view, info); 391 } 392 } 393 394 private void dispatchVisibilityAggregated(View view, boolean isVisible) { 395 // Similar to View.dispatchVisibilityAggregated implementation. 396 final boolean thisVisible = view.getVisibility() == VISIBLE; 397 if (thisVisible || !isVisible) { 398 view.onVisibilityAggregated(isVisible); 399 } 400 401 if (view instanceof ViewGroup) { 402 isVisible = thisVisible && isVisible; 403 ViewGroup vg = (ViewGroup) view; 404 int count = vg.getChildCount(); 405 406 for (int i = 0; i < count; i++) { 407 dispatchVisibilityAggregated(vg.getChildAt(i), isVisible); 408 } 409 } 410 } 411 412 private void populate(BgDataModel dataModel, 413 Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) { 414 // Separate the items that are on the current screen, and the other remaining items. 415 ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>(); 416 ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>(); 417 ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>(); 418 ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>(); 419 filterCurrentWorkspaceItems(0 /* currentScreenId */, 420 dataModel.workspaceItems, currentWorkspaceItems, 421 otherWorkspaceItems); 422 filterCurrentWorkspaceItems(0 /* currentScreenId */, dataModel.appWidgets, 423 currentAppWidgets, otherAppWidgets); 424 sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems); 425 for (ItemInfo itemInfo : currentWorkspaceItems) { 426 switch (itemInfo.itemType) { 427 case Favorites.ITEM_TYPE_APPLICATION: 428 case Favorites.ITEM_TYPE_SHORTCUT: 429 case Favorites.ITEM_TYPE_DEEP_SHORTCUT: 430 inflateAndAddIcon((WorkspaceItemInfo) itemInfo); 431 break; 432 case Favorites.ITEM_TYPE_FOLDER: 433 inflateAndAddFolder((FolderInfo) itemInfo); 434 break; 435 default: 436 break; 437 } 438 } 439 for (ItemInfo itemInfo : currentAppWidgets) { 440 switch (itemInfo.itemType) { 441 case Favorites.ITEM_TYPE_APPWIDGET: 442 case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: 443 if (widgetProviderInfoMap != null) { 444 inflateAndAddWidgets( 445 (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap); 446 } else { 447 inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo, 448 dataModel.widgetsModel); 449 } 450 break; 451 default: 452 break; 453 } 454 } 455 IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems, 456 mDp.numShownHotseatIcons); 457 FixedContainerItems hotseatpredictions = 458 dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION); 459 List<ItemInfo> predictions = hotseatpredictions == null 460 ? Collections.emptyList() : hotseatpredictions.items; 461 int count = Math.min(ranks.size(), predictions.size()); 462 for (int i = 0; i < count; i++) { 463 int rank = ranks.get(i); 464 WorkspaceItemInfo itemInfo = 465 new WorkspaceItemInfo((WorkspaceItemInfo) predictions.get(i)); 466 itemInfo.container = CONTAINER_HOTSEAT_PREDICTION; 467 itemInfo.rank = rank; 468 itemInfo.cellX = mHotseat.getCellXFromOrder(rank); 469 itemInfo.cellY = mHotseat.getCellYFromOrder(rank); 470 itemInfo.screenId = rank; 471 inflateAndAddPredictedIcon(itemInfo); 472 } 473 474 // Add first page QSB 475 if (FeatureFlags.QSB_ON_FIRST_SCREEN) { 476 View qsb = mHomeElementInflater.inflate( 477 R.layout.search_container_workspace, mWorkspace, false); 478 CellLayout.LayoutParams lp = 479 new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1); 480 lp.canReorder = false; 481 mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true); 482 } 483 484 measureView(mRootView, mDp.widthPx, mDp.heightPx); 485 dispatchVisibilityAggregated(mRootView, true); 486 measureView(mRootView, mDp.widthPx, mDp.heightPx); 487 // Additional measure for views which use auto text size API 488 measureView(mRootView, mDp.widthPx, mDp.heightPx); 489 } 490 491 private static void measureView(View view, int width, int height) { 492 view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); 493 view.layout(0, 0, width, height); 494 } 495 496 /** Root layout for launcher preview that intercepts all touch events. */ 497 public static class LauncherPreviewLayout extends InsettableFrameLayout { 498 public LauncherPreviewLayout(Context context, AttributeSet attrs) { 499 super(context, attrs); 500 } 501 502 @Override 503 public boolean onInterceptTouchEvent(MotionEvent ev) { 504 return true; 505 } 506 } 507 } 508