1 /* 2 * Copyright (C) 2019 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.wallpaper.picker; 17 18 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; 19 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; 20 import static android.view.View.MeasureSpec.EXACTLY; 21 import static android.view.View.MeasureSpec.makeMeasureSpec; 22 23 import static com.android.wallpaper.util.WallpaperSurfaceCallback.LOW_RES_BITMAP_BLUR_RADIUS; 24 import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY; 25 import static com.android.wallpaper.widget.BottomActionBar.BottomAction.EDIT; 26 import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION; 27 28 import android.animation.Animator; 29 import android.animation.AnimatorListenerAdapter; 30 import android.app.Activity; 31 import android.app.WallpaperColors; 32 import android.content.Context; 33 import android.content.res.Resources; 34 import android.graphics.Bitmap; 35 import android.graphics.BitmapFactory; 36 import android.graphics.Color; 37 import android.graphics.ColorSpace; 38 import android.graphics.Point; 39 import android.graphics.PointF; 40 import android.graphics.Rect; 41 import android.graphics.RenderEffect; 42 import android.graphics.Shader; 43 import android.os.Bundle; 44 import android.os.Handler; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.Surface; 48 import android.view.SurfaceControlViewHost; 49 import android.view.SurfaceHolder; 50 import android.view.SurfaceView; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.view.ViewGroup.LayoutParams; 54 import android.widget.ImageView; 55 56 import androidx.annotation.NonNull; 57 import androidx.annotation.Nullable; 58 import androidx.annotation.VisibleForTesting; 59 import androidx.cardview.widget.CardView; 60 import androidx.constraintlayout.widget.ConstraintLayout; 61 import androidx.constraintlayout.widget.ConstraintSet; 62 import androidx.fragment.app.FragmentActivity; 63 64 import com.android.wallpaper.R; 65 import com.android.wallpaper.asset.Asset; 66 import com.android.wallpaper.asset.CurrentWallpaperAssetVN; 67 import com.android.wallpaper.model.SetWallpaperViewModel; 68 import com.android.wallpaper.model.WallpaperInfo.ColorInfo; 69 import com.android.wallpaper.module.BitmapCropper; 70 import com.android.wallpaper.module.Injector; 71 import com.android.wallpaper.module.InjectorProvider; 72 import com.android.wallpaper.module.LargeScreenMultiPanesChecker; 73 import com.android.wallpaper.module.WallpaperPersister.Destination; 74 import com.android.wallpaper.module.WallpaperPreferences; 75 import com.android.wallpaper.util.DisplayUtils; 76 import com.android.wallpaper.util.FullScreenAnimation; 77 import com.android.wallpaper.util.ResourceUtils; 78 import com.android.wallpaper.util.ScreenSizeCalculator; 79 import com.android.wallpaper.util.SizeCalculator; 80 import com.android.wallpaper.util.WallpaperCropUtils; 81 import com.android.wallpaper.widget.BottomActionBar; 82 import com.android.wallpaper.widget.BottomActionBar.AccessibilityCallback; 83 import com.android.wallpaper.widget.LockScreenPreviewer; 84 85 import com.bumptech.glide.Glide; 86 import com.bumptech.glide.MemoryCategory; 87 import com.davemorrissey.labs.subscaleview.ImageSource; 88 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; 89 90 import java.io.ByteArrayOutputStream; 91 import java.util.Locale; 92 import java.util.concurrent.ExecutionException; 93 import java.util.concurrent.Executor; 94 import java.util.concurrent.Executors; 95 import java.util.concurrent.Future; 96 import java.util.concurrent.atomic.AtomicInteger; 97 98 /** 99 * Fragment which displays the UI for previewing an individual static wallpaper and its attribution 100 * information. 101 */ 102 public class ImagePreviewFragment extends PreviewFragment { 103 104 private static final String TAG = "ImagePreviewFragment"; 105 private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f; 106 private static final Executor sExecutor = Executors.newCachedThreadPool(); 107 108 private final WallpaperSurfaceCallback mWallpaperSurfaceCallback = 109 new WallpaperSurfaceCallback(); 110 111 private final AtomicInteger mImageScaleChangeCounter = new AtomicInteger(0); 112 private final AtomicInteger mRecalculateColorCounter = new AtomicInteger(0); 113 private final Injector mInjector = InjectorProvider.getInjector(); 114 115 /** 116 * Size of the screen considered for cropping the wallpaper (typically the same as 117 * {@link #mScreenSize} but it could be different on multi-display) 118 */ 119 private Point mWallpaperScreenSize; 120 /** 121 * The size of the current screen 122 */ 123 private Point mScreenSize; 124 protected Point mRawWallpaperSize; // Native size of wallpaper image. 125 protected ImageView mLowResImageView; 126 protected TouchForwardingLayout mTouchForwardingLayout; 127 protected ConstraintLayout mContainer; 128 protected SurfaceView mWallpaperSurface; 129 private boolean mIsSurfaceCreated = false; 130 private WallpaperColors mWallpaperColors; 131 private WallpaperPreferences mWallpaperPreferences; 132 133 protected SurfaceView mWorkspaceSurface; 134 protected WorkspaceSurfaceHolderCallback mWorkspaceSurfaceCallback; 135 protected ViewGroup mLockPreviewContainer; 136 protected LockScreenPreviewer mLockScreenPreviewer; 137 protected SubsamplingScaleImageView mFullResImageView; 138 protected Asset mWallpaperAsset; 139 private Future<ColorInfo> mColorFuture; 140 private DisplayUtils mDisplayUtils; 141 142 @Override onCreate(Bundle savedInstanceState)143 public void onCreate(Bundle savedInstanceState) { 144 super.onCreate(savedInstanceState); 145 mWallpaperAsset = mWallpaper.getAsset(requireContext().getApplicationContext()); 146 mColorFuture = mWallpaper.computeColorInfo(requireContext()); 147 mWallpaperPreferences = mInjector.getPreferences(getContext()); 148 } 149 150 @Override getLayoutResId()151 protected int getLayoutResId() { 152 return R.layout.fragment_image_preview; 153 } 154 155 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)156 public View onCreateView(LayoutInflater inflater, ViewGroup container, 157 Bundle savedInstanceState) { 158 View view = super.onCreateView(inflater, container, savedInstanceState); 159 160 Activity activity = requireActivity(); 161 mDisplayUtils = mInjector.getDisplayUtils(activity); 162 ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance(); 163 mScreenSize = screenSizeCalculator.getScreenSize( 164 activity.getWindowManager().getDefaultDisplay()); 165 // "Wallpaper screen" size will be the size of the largest screen available 166 mWallpaperScreenSize = screenSizeCalculator.getScreenSize( 167 mDisplayUtils.getWallpaperDisplay()); 168 169 mContainer = view.findViewById(R.id.container); 170 mTouchForwardingLayout = mContainer.findViewById(R.id.touch_forwarding_layout); 171 mTouchForwardingLayout.setForwardingEnabled(true); 172 173 // Update preview header color which covers toolbar and status bar area. 174 updatePreviewHeader(view); 175 176 // Set aspect ratio on the preview card dynamically. 177 ConstraintSet set = new ConstraintSet(); 178 set.clone(mContainer); 179 String ratio = String.format(Locale.US, "%d:%d", mScreenSize.x, mScreenSize.y); 180 set.setDimensionRatio(mTouchForwardingLayout.getId(), ratio); 181 set.applyTo(mContainer); 182 183 mWorkspaceSurface = mContainer.findViewById(R.id.workspace_surface); 184 mWorkspaceSurfaceCallback = createWorkspaceSurfaceCallback(mWorkspaceSurface); 185 mWallpaperSurface = mContainer.findViewById(R.id.wallpaper_surface); 186 mLockPreviewContainer = mContainer.findViewById(R.id.lock_screen_preview_container); 187 int placeHolderColor = ResourceUtils.getColorAttr(getContext(), 188 android.R.attr.colorBackground); 189 mWorkspaceSurface.setResizeBackgroundColor(placeHolderColor); 190 mLockScreenPreviewer = new LockScreenPreviewer(getLifecycle(), getContext(), 191 mLockPreviewContainer); 192 mLockScreenPreviewer.setDateViewVisibility(!mFullScreenAnimation.isFullScreen()); 193 mFullScreenAnimation.setFullScreenStatusListener( 194 isFullScreen -> { 195 mLockScreenPreviewer.setDateViewVisibility(!isFullScreen); 196 if (!isFullScreen) { 197 mBottomActionBar.focusAccessibilityAction(EDIT); 198 } 199 }); 200 setUpTabs(view.findViewById(R.id.separated_tabs)); 201 202 view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 203 @Override 204 public void onLayoutChange(View thisView, int left, int top, int right, int bottom, 205 int oldLeft, int oldTop, int oldRight, int oldBottom) { 206 ((CardView) mWorkspaceSurface.getParent()).setRadius( 207 SizeCalculator.getPreviewCornerRadius(activity, 208 ((CardView) mWorkspaceSurface.getParent()).getMeasuredWidth())); 209 view.removeOnLayoutChangeListener(this); 210 } 211 }); 212 213 renderImageWallpaper(); 214 renderWorkspaceSurface(); 215 216 // Trim some memory from Glide to make room for the full-size image in this fragment. 217 Glide.get(activity).setMemoryCategory(MemoryCategory.LOW); 218 219 return view; 220 } 221 222 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)223 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 224 super.onViewCreated(view, savedInstanceState); 225 } 226 227 @VisibleForTesting(otherwise = VisibleForTesting.NONE) getFullResImageView()228 public SubsamplingScaleImageView getFullResImageView() { 229 return mFullResImageView; 230 } 231 onWallpaperColorsChanged(@ullable WallpaperColors colors)232 protected void onWallpaperColorsChanged(@Nullable WallpaperColors colors) { 233 // Make it enabled since the buttons are disabled while wallpaper is moving. 234 mBottomActionBar.enableActionButtonsWithBottomSheet(true); 235 236 mWallpaperColors = colors; 237 mLockScreenPreviewer.setColor(colors); 238 239 mFullScreenAnimation.setFullScreenTextColor( 240 colors == null || (colors.getColorHints() 241 & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0 242 ? FullScreenAnimation.FullScreenTextColor.LIGHT 243 : FullScreenAnimation.FullScreenTextColor.DARK); 244 } 245 246 @Override isLoaded()247 protected boolean isLoaded() { 248 return mFullResImageView != null && mFullResImageView.hasImage(); 249 } 250 251 @Override onClickOk()252 public void onClickOk() { 253 FragmentActivity activity = getActivity(); 254 if (activity != null) { 255 activity.finish(); 256 } 257 } 258 259 @Override onDestroy()260 public void onDestroy() { 261 super.onDestroy(); 262 263 if (mFullResImageView != null) { 264 mFullResImageView.recycle(); 265 } 266 267 if (mLockScreenPreviewer != null) { 268 mLockScreenPreviewer.release(); 269 } 270 271 mWallpaperSurfaceCallback.cleanUp(); 272 mWorkspaceSurfaceCallback.cleanUp(); 273 } 274 setupActionBar()275 protected void setupActionBar() { 276 mBottomActionBar.bindBottomSheetContentWithAction( 277 new WallpaperInfoContent(getContext()), INFORMATION); 278 Activity activity = getActivity(); 279 LargeScreenMultiPanesChecker checker = new LargeScreenMultiPanesChecker(); 280 if (activity != null 281 && (activity.isInMultiWindowMode() || checker.isMultiPanesEnabled(getContext()))) { 282 mBottomActionBar.showActionsOnly(INFORMATION, APPLY); 283 } else { 284 mBottomActionBar.showActionsOnly(INFORMATION, EDIT, APPLY); 285 } 286 mBottomActionBar.setActionClickListener(APPLY, 287 unused -> onSetWallpaperClicked(null, mWallpaper)); 288 } 289 290 @Override onBottomActionBarReady(BottomActionBar bottomActionBar)291 protected void onBottomActionBarReady(BottomActionBar bottomActionBar) { 292 super.onBottomActionBarReady(bottomActionBar); 293 setupActionBar(); 294 View separatedTabsContainer = getView().findViewById(R.id.separated_tabs_container); 295 // Update target view's accessibility param since it will be blocked by the bottom sheet 296 // when expanded. 297 mBottomActionBar.setAccessibilityCallback(new AccessibilityCallback() { 298 @Override 299 public void onBottomSheetCollapsed() { 300 mContainer.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 301 separatedTabsContainer.setImportantForAccessibility( 302 IMPORTANT_FOR_ACCESSIBILITY_YES); 303 } 304 305 @Override 306 public void onBottomSheetExpanded() { 307 mContainer.setImportantForAccessibility( 308 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 309 separatedTabsContainer.setImportantForAccessibility( 310 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 311 312 } 313 }); 314 315 mBottomActionBar.show(); 316 // To avoid applying the wallpaper when the wallpaper's not parsed. 317 mBottomActionBar.disableActions(); 318 // If the wallpaper is parsed, enable the bottom action bar. 319 if (mRawWallpaperSize != null) { 320 mBottomActionBar.enableActions(); 321 } 322 } 323 324 /** 325 * Initializes MosaicView by initializing tiling, setting a fallback page bitmap, and 326 * initializing a zoom-scroll observer and click listener. 327 */ initFullResView()328 private synchronized void initFullResView() { 329 if (mRawWallpaperSize == null || mFullResImageView == null 330 || mFullResImageView.isImageLoaded()) { 331 return; 332 } 333 final boolean isWallpaperColorCached = isWallpaperColorInCache( 334 mWallpaper.getStoredWallpaperId(getContext())); 335 336 // If the color is cached, get the colors from SharedPreferences. 337 if (isWallpaperColorCached) { 338 Handler.getMain().post(() -> onWallpaperColorsChanged( 339 mWallpaperPreferences.getWallpaperColors( 340 mWallpaper.getStoredWallpaperId(getContext())))); 341 } 342 343 // Minimum scale will only be respected under this scale type. 344 mFullResImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM); 345 // When we set a minimum scale bigger than the scale with which the full image is shown, 346 // disallow user to pan outside the view we show the wallpaper in. 347 mFullResImageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE); 348 349 // Then set a fallback "page bitmap" to cover the whole MosaicView, which is an actual 350 // (lower res) version of the image to be displayed. 351 Point targetPageBitmapSize = new Point(mRawWallpaperSize); 352 mWallpaperAsset.decodeBitmap(targetPageBitmapSize.x, targetPageBitmapSize.y, 353 pageBitmap -> { 354 // Check that the activity is still around since the decoding task started. 355 if (getActivity() == null) { 356 return; 357 } 358 359 // The page bitmap may be null if there was a decoding error, so show an 360 // error dialog. 361 if (pageBitmap == null) { 362 showLoadWallpaperErrorDialog(); 363 return; 364 } 365 // Some of these may be null depending on if the Fragment is paused, stopped, 366 // or destroyed. 367 mWallpaperSurface.setBackgroundColor(Color.TRANSPARENT); 368 if (mFullResImageView != null) { 369 // Set page bitmap. 370 mFullResImageView.setImage(ImageSource.bitmap(pageBitmap)); 371 372 if (isWallpaperColorCached) { 373 crossFadeInMosaicView(); 374 } else { 375 // Hide full image view then show it when wallpaper color is updated 376 mFullResImageView.setAlpha(0f); 377 } 378 379 setDefaultWallpaperZoomAndScroll( 380 mWallpaperAsset instanceof CurrentWallpaperAssetVN); 381 mFullResImageView.setOnStateChangedListener( 382 new SubsamplingScaleImageView.DefaultOnStateChangedListener() { 383 @Override 384 public void onCenterChanged(PointF newCenter, int origin) { 385 super.onCenterChanged(newCenter, origin); 386 // Disallow bottom sheet to popup when wallpaper is moving 387 // by user dragging. 388 mBottomActionBar.enableActionButtonsWithBottomSheet(false); 389 mImageScaleChangeCounter.incrementAndGet(); 390 mFullResImageView.postDelayed(() -> { 391 if (mImageScaleChangeCounter.decrementAndGet() == 0) { 392 recalculateColors(false); 393 } 394 }, /* delayMillis= */ 100); 395 } 396 }); 397 398 // If the color isn't cached in SharedPreference, recalculate the Colors. 399 if (!isWallpaperColorCached) { 400 Handler.getMain().post(() -> { 401 recalculateColors(true); 402 }); 403 } 404 } 405 }); 406 407 mFullResImageView.setOnTouchListener((v, ev) -> { 408 // Consume the touch event for collapsing bottom sheet while it is expanded or 409 // dragging (not collapsed). 410 if (mBottomActionBar != null && !mBottomActionBar.isBottomSheetCollapsed()) { 411 mBottomActionBar.collapseBottomSheetIfExpanded(); 412 return true; 413 } 414 return false; 415 }); 416 } 417 recalculateColors(boolean cacheColor)418 private void recalculateColors(boolean cacheColor) { 419 Context context = getContext(); 420 if (context == null) { 421 Log.e(TAG, "Got null context, skip recalculating colors"); 422 return; 423 } 424 425 BitmapCropper bitmapCropper = mInjector.getBitmapCropper(); 426 bitmapCropper.cropAndScaleBitmap(mWallpaperAsset, mFullResImageView.getScale(), 427 calculateCropRect(context, /* cropExtraWidth= */ true), /* adjustForRtl= */ false, 428 new BitmapCropper.Callback() { 429 @Override 430 public void onBitmapCropped(Bitmap croppedBitmap) { 431 mRecalculateColorCounter.incrementAndGet(); 432 sExecutor.execute(() -> { 433 boolean shouldRecycle = false; 434 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(); 435 Bitmap cropped = croppedBitmap; 436 if (cropped.compress(Bitmap.CompressFormat.PNG, 100, tmpOut)) { 437 byte[] outByteArray = tmpOut.toByteArray(); 438 BitmapFactory.Options options = new BitmapFactory.Options(); 439 options.inPreferredColorSpace = 440 ColorSpace.get(ColorSpace.Named.SRGB); 441 cropped = BitmapFactory.decodeByteArray(outByteArray, 0, 442 outByteArray.length); 443 } 444 if (cropped.getConfig() == Bitmap.Config.HARDWARE) { 445 cropped = cropped.copy(Bitmap.Config.ARGB_8888, false); 446 shouldRecycle = true; 447 } 448 WallpaperColors colors = WallpaperColors.fromBitmap(cropped); 449 if (shouldRecycle) { 450 cropped.recycle(); 451 } 452 if (mRecalculateColorCounter.decrementAndGet() == 0) { 453 Handler.getMain().post(() -> { 454 onWallpaperColorsChanged(colors); 455 if (mFullResImageView.getAlpha() == 0f) { 456 crossFadeInMosaicView(); 457 } 458 }); 459 } 460 461 if (cacheColor) { 462 mWallpaperPreferences.storeWallpaperColors( 463 mWallpaper.getStoredWallpaperId(context), colors); 464 } 465 }); 466 } 467 468 @Override 469 public void onError(@Nullable Throwable e) { 470 Log.w(TAG, "Recalculate colors, crop and scale bitmap failed.", e); 471 } 472 }); 473 } 474 475 /** 476 * Makes the MosaicView visible with an alpha fade-in animation while fading out the loading 477 * indicator. 478 */ crossFadeInMosaicView()479 private void crossFadeInMosaicView() { 480 if (getActivity() != null && isAdded()) { 481 long shortAnimationDuration = getResources().getInteger( 482 android.R.integer.config_shortAnimTime); 483 484 mFullResImageView.setAlpha(0f); 485 mFullResImageView.animate() 486 .alpha(1f) 487 .setInterpolator(ALPHA_OUT) 488 .setDuration(shortAnimationDuration) 489 .setListener(new AnimatorListenerAdapter() { 490 @Override 491 public void onAnimationEnd(Animator animation) { 492 // Clear the thumbnail bitmap reference to save memory since it's no 493 // longer visible. 494 if (mLowResImageView != null) { 495 mLowResImageView.setImageBitmap(null); 496 } 497 } 498 }); 499 } 500 } 501 502 /** 503 * Sets the default wallpaper zoom and scroll position based on a "crop surface" (with extra 504 * width to account for parallax) superimposed on the screen. Shows as much of the wallpaper as 505 * possible on the crop surface and align screen to crop surface such that the default preview 506 * matches what would be seen by the user in the left-most home screen. 507 * 508 * <p>This method is called once in the Fragment lifecycle after the wallpaper asset has loaded 509 * and rendered to the layout. 510 * 511 * @param offsetToStart {@code true} if we want to offset the visible rectangle to the start 512 * side of the raw wallpaper; {@code false} otherwise. 513 */ setDefaultWallpaperZoomAndScroll(boolean offsetToStart)514 private void setDefaultWallpaperZoomAndScroll(boolean offsetToStart) { 515 // Determine minimum zoom to fit maximum visible area of wallpaper on crop surface. 516 int cropWidth = mWallpaperSurface.getMeasuredWidth(); 517 int cropHeight = mWallpaperSurface.getMeasuredHeight(); 518 Point crop = new Point(cropWidth, cropHeight); 519 Rect visibleRawWallpaperRect = 520 WallpaperCropUtils.calculateVisibleRect(mRawWallpaperSize, crop); 521 if (offsetToStart && mDisplayUtils.isOnWallpaperDisplay(requireActivity())) { 522 if (WallpaperCropUtils.isRtl(requireContext())) { 523 visibleRawWallpaperRect.offsetTo(mRawWallpaperSize.x 524 - visibleRawWallpaperRect.width(), visibleRawWallpaperRect.top); 525 } else { 526 visibleRawWallpaperRect.offsetTo(/* newLeft= */ 0, visibleRawWallpaperRect.top); 527 } 528 } 529 530 final PointF centerPosition = new PointF(visibleRawWallpaperRect.centerX(), 531 visibleRawWallpaperRect.centerY()); 532 533 Point visibleRawWallpaperSize = new Point(visibleRawWallpaperRect.width(), 534 visibleRawWallpaperRect.height()); 535 536 final float defaultWallpaperZoom = WallpaperCropUtils.calculateMinZoom( 537 visibleRawWallpaperSize, crop); 538 final float minWallpaperZoom = defaultWallpaperZoom; 539 540 541 // Set min wallpaper zoom and max zoom on MosaicView widget. 542 mFullResImageView.setMaxScale(Math.max(DEFAULT_WALLPAPER_MAX_ZOOM, defaultWallpaperZoom)); 543 mFullResImageView.setMinScale(minWallpaperZoom); 544 545 // Set center to composite positioning between scaled wallpaper and screen. 546 mFullResImageView.setScaleAndCenter(minWallpaperZoom, centerPosition); 547 } 548 calculateCropRect(Context context, boolean cropExtraWidth)549 private Rect calculateCropRect(Context context, boolean cropExtraWidth) { 550 float wallpaperZoom = mFullResImageView.getScale(); 551 Context appContext = context.getApplicationContext(); 552 553 Rect visibleFileRect = new Rect(); 554 mFullResImageView.visibleFileRect(visibleFileRect); 555 556 int cropWidth = mWallpaperSurface.getMeasuredWidth(); 557 int cropHeight = mWallpaperSurface.getMeasuredHeight(); 558 int maxCrop = Math.max(cropWidth, cropHeight); 559 int minCrop = Math.min(cropWidth, cropHeight); 560 Point hostViewSize = new Point(cropWidth, cropHeight); 561 562 Resources res = appContext.getResources(); 563 Point cropSurfaceSize = WallpaperCropUtils.calculateCropSurfaceSize(res, maxCrop, minCrop, 564 cropWidth, cropHeight); 565 return WallpaperCropUtils.calculateCropRect(appContext, hostViewSize, 566 cropSurfaceSize, mRawWallpaperSize, visibleFileRect, wallpaperZoom, cropExtraWidth); 567 } 568 569 @Override setCurrentWallpaper(@estination int destination)570 protected void setCurrentWallpaper(@Destination int destination) { 571 // Only crop extra wallpaper width for single display devices. 572 Rect cropRect = calculateCropRect(getContext(), !mDisplayUtils.hasMultiInternalDisplays()); 573 float screenScale = WallpaperCropUtils.getScaleOfScreenResolution( 574 mFullResImageView.getScale(), cropRect, mWallpaperScreenSize.x, 575 mWallpaperScreenSize.y); 576 Rect scaledCropRect = new Rect( 577 Math.round((float) cropRect.left * screenScale), 578 Math.round((float) cropRect.top * screenScale), 579 Math.round((float) cropRect.right * screenScale), 580 Math.round((float) cropRect.bottom * screenScale)); 581 mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, mWallpaperAsset, 582 destination, mFullResImageView.getScale() * screenScale, scaledCropRect, 583 mWallpaperColors, SetWallpaperViewModel.getCallback(mViewModelProvider)); 584 } 585 renderWorkspaceSurface()586 private void renderWorkspaceSurface() { 587 mWorkspaceSurface.setZOrderMediaOverlay(true); 588 mWorkspaceSurface.getHolder().addCallback(mWorkspaceSurfaceCallback); 589 } 590 renderImageWallpaper()591 private void renderImageWallpaper() { 592 mWallpaperSurface.getHolder().addCallback(mWallpaperSurfaceCallback); 593 } 594 595 private class WallpaperSurfaceCallback implements SurfaceHolder.Callback { 596 private Surface mLastSurface; 597 private SurfaceControlViewHost mHost; 598 599 @Override surfaceCreated(SurfaceHolder holder)600 public void surfaceCreated(SurfaceHolder holder) { 601 if (mLastSurface != holder.getSurface()) { 602 mLastSurface = holder.getSurface(); 603 if (mFullResImageView != null) { 604 mFullResImageView.recycle(); 605 } 606 Context context = getContext(); 607 View wallpaperPreviewContainer = LayoutInflater.from(context).inflate( 608 R.layout.fullscreen_wallpaper_preview, null); 609 mFullResImageView = wallpaperPreviewContainer.findViewById(R.id.full_res_image); 610 mLowResImageView = wallpaperPreviewContainer.findViewById(R.id.low_res_image); 611 mLowResImageView.setRenderEffect( 612 RenderEffect.createBlurEffect(LOW_RES_BITMAP_BLUR_RADIUS, 613 LOW_RES_BITMAP_BLUR_RADIUS, Shader.TileMode.CLAMP)); 614 mWallpaperAsset.decodeRawDimensions(getActivity(), dimensions -> { 615 // Don't continue loading the wallpaper if the Fragment is detached. 616 if (getActivity() == null) { 617 return; 618 } 619 620 // Return early and show a dialog if dimensions are null (signaling a decoding 621 // error). 622 if (dimensions == null) { 623 showLoadWallpaperErrorDialog(); 624 return; 625 } 626 627 // To avoid applying the wallpaper when it's not parsed. Now it's parsed, enable 628 // the bottom action bar to allow applying the wallpaper. 629 if (mBottomActionBar != null) { 630 mBottomActionBar.enableActions(); 631 } 632 633 mRawWallpaperSize = dimensions; 634 initFullResView(); 635 }); 636 637 // Calculate the size of mWallpaperSurface based on system zoom's scale and 638 // on the larger screen size (if more than one) so that the wallpaper is 639 // rendered in a larger surface than what preview shows, simulating the behavior of 640 // the actual wallpaper surface and so we can crop it to a size that fits in all 641 // screens. 642 float scale = WallpaperCropUtils.getSystemWallpaperMaximumScale(context); 643 int origWidth = mWallpaperSurface.getWidth(); 644 int origHeight = mWallpaperSurface.getHeight(); 645 646 int scaledOrigWidth = origWidth; 647 if (!mDisplayUtils.isOnWallpaperDisplay(requireActivity())) { 648 // Scale the width of the mWallpaperSurface if the current screen is not the 649 // largest screen (wallpaper screen). 650 float previewToScreenScale = (float) origWidth / mScreenSize.x; 651 scaledOrigWidth = (int) (mWallpaperScreenSize.x * previewToScreenScale); 652 } 653 int width = (int) (scaledOrigWidth * scale); 654 int height = (int) (origHeight * scale); 655 int left = (origWidth - width) / 2; 656 int top = (origHeight - height) / 2; 657 658 if (WallpaperCropUtils.isRtl(context)) { 659 left *= -1; 660 } 661 662 LayoutParams params = mWallpaperSurface.getLayoutParams(); 663 params.width = width; 664 params.height = height; 665 mWallpaperSurface.setX(left); 666 mWallpaperSurface.setY(top); 667 mWallpaperSurface.setLayoutParams(params); 668 mWallpaperSurface.requestLayout(); 669 670 // Load a low-res placeholder image if there's a thumbnail available from the asset 671 // that can be shown to the user more quickly than the full-sized image. 672 Activity activity = requireActivity(); 673 // Change to background color if colorValue is Color.TRANSPARENT 674 int placeHolderColor = ResourceUtils.getColorAttr(activity, 675 android.R.attr.colorBackground); 676 if (mColorFuture.isDone()) { 677 try { 678 int colorValue = mColorFuture.get().getPlaceholderColor(); 679 if (colorValue != Color.TRANSPARENT) { 680 placeHolderColor = colorValue; 681 } 682 } catch (InterruptedException | ExecutionException e) { 683 // Do nothing 684 } 685 } 686 mWallpaperSurface.setResizeBackgroundColor(placeHolderColor); 687 mWallpaperSurface.setBackgroundColor(placeHolderColor); 688 mLowResImageView.setBackgroundColor(placeHolderColor); 689 690 mWallpaperAsset.loadLowResDrawable(activity, mLowResImageView, placeHolderColor, 691 mPreviewBitmapTransformation); 692 693 wallpaperPreviewContainer.measure( 694 makeMeasureSpec(width, EXACTLY), 695 makeMeasureSpec(height, EXACTLY)); 696 wallpaperPreviewContainer.layout(0, 0, width, height); 697 mTouchForwardingLayout.setTargetView(mFullResImageView); 698 699 cleanUp(); 700 mHost = new SurfaceControlViewHost(context, 701 context.getDisplay(), mWallpaperSurface.getHostToken()); 702 mHost.setView(wallpaperPreviewContainer, wallpaperPreviewContainer.getWidth(), 703 wallpaperPreviewContainer.getHeight()); 704 mWallpaperSurface.setChildSurfacePackage(mHost.getSurfacePackage()); 705 // After surface creating, update workspaceSurface. 706 mIsSurfaceCreated = true; 707 updateScreenPreview(mLastSelectedTabPositionOptional.orElse(0) == 0); 708 } 709 } 710 711 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)712 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } 713 714 @Override surfaceDestroyed(SurfaceHolder holder)715 public void surfaceDestroyed(SurfaceHolder holder) { } 716 cleanUp()717 public void cleanUp() { 718 if (mHost != null) { 719 mHost.release(); 720 mHost = null; 721 } 722 mIsSurfaceCreated = false; 723 } 724 } 725 isWallpaperColorInCache(String storedWallpaperId)726 private boolean isWallpaperColorInCache(String storedWallpaperId) { 727 if (storedWallpaperId == null || mWallpaperPreferences.getWallpaperColors( 728 storedWallpaperId) == null) { 729 return false; 730 } 731 return true; 732 } 733 734 @Override updateScreenPreview(boolean isHomeSelected)735 protected void updateScreenPreview(boolean isHomeSelected) { 736 // Use View.GONE for WorkspaceSurface's visibility before its surface is created. 737 mWorkspaceSurface.setVisibility(isHomeSelected && mIsSurfaceCreated ? View.VISIBLE : 738 View.GONE); 739 740 mLockPreviewContainer.setVisibility(isHomeSelected ? View.INVISIBLE : View.VISIBLE); 741 742 mFullScreenAnimation.setIsHomeSelected(isHomeSelected); 743 } 744 } 745