• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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