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