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