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 android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.app.Activity;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.Bitmap.Config;
24 import android.graphics.Color;
25 import android.graphics.Point;
26 import android.graphics.PointF;
27 import android.graphics.Rect;
28 import android.os.Bundle;
29 import android.view.Display;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.ImageView;
34 
35 import androidx.annotation.Nullable;
36 import androidx.fragment.app.FragmentActivity;
37 
38 import com.android.wallpaper.R;
39 import com.android.wallpaper.asset.Asset;
40 import com.android.wallpaper.module.WallpaperPersister.Destination;
41 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
42 import com.android.wallpaper.util.ScreenSizeCalculator;
43 import com.android.wallpaper.util.WallpaperCropUtils;
44 
45 import com.bumptech.glide.Glide;
46 import com.bumptech.glide.MemoryCategory;
47 import com.davemorrissey.labs.subscaleview.ImageSource;
48 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
49 import com.google.android.material.bottomsheet.BottomSheetBehavior;
50 
51 /**
52  * Fragment which displays the UI for previewing an individual static wallpaper and its attribution
53  * information.
54  */
55 public class ImagePreviewFragment extends PreviewFragment {
56 
57     private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f;
58 
59     private SubsamplingScaleImageView mFullResImageView;
60     private Asset mWallpaperAsset;
61     private Point mDefaultCropSurfaceSize;
62     private Point mScreenSize;
63     private Point mRawWallpaperSize; // Native size of wallpaper image.
64     private ImageView mLowResImageView;
65 
66     private InfoPageController mInfoPageController;
67 
68     @Override
onCreate(Bundle savedInstanceState)69     public void onCreate(Bundle savedInstanceState) {
70         super.onCreate(savedInstanceState);
71         mWallpaperAsset = mWallpaper.getAsset(requireContext().getApplicationContext());
72     }
73 
74     @Override
getLayoutResId()75     protected int getLayoutResId() {
76         return R.layout.fragment_image_preview;
77     }
78 
79 
getBottomSheetResId()80     protected int getBottomSheetResId() {
81         return R.id.bottom_sheet;
82     }
83 
84     @Override
getLoadingIndicatorResId()85     protected int getLoadingIndicatorResId() {
86         return R.id.loading_indicator;
87     }
88 
89     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)90     public View onCreateView(LayoutInflater inflater, ViewGroup container,
91                              Bundle savedInstanceState) {
92         View view = super.onCreateView(inflater, container, savedInstanceState);
93 
94         Activity activity = requireActivity();
95 
96         mFullResImageView = view.findViewById(R.id.full_res_image);
97 
98         mInfoPageController = new InfoPageController(view.findViewById(R.id.page_info),
99                 mPreviewMode);
100 
101         mLowResImageView = view.findViewById(R.id.low_res_image);
102 
103         // Trim some memory from Glide to make room for the full-size image in this fragment.
104         Glide.get(activity).setMemoryCategory(MemoryCategory.LOW);
105 
106         mDefaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
107                 getResources(), activity.getWindowManager().getDefaultDisplay());
108         mScreenSize = ScreenSizeCalculator.getInstance().getScreenSize(
109                 activity.getWindowManager().getDefaultDisplay());
110 
111         // Load a low-res placeholder image if there's a thumbnail available from the asset that can
112         // be shown to the user more quickly than the full-sized image.
113         if (mWallpaperAsset.hasLowResDataSource()) {
114             mWallpaperAsset.loadLowResDrawable(activity, mLowResImageView, Color.BLACK,
115                     new WallpaperPreviewBitmapTransformation(activity.getApplicationContext(),
116                             isRtl()));
117         }
118 
119         mWallpaperAsset.decodeRawDimensions(getActivity(), dimensions -> {
120             // Don't continue loading the wallpaper if the Fragment is detached.
121             if (getActivity() == null) {
122                 return;
123             }
124 
125             // Return early and show a dialog if dimensions are null (signaling a decoding error).
126             if (dimensions == null) {
127                 showLoadWallpaperErrorDialog();
128                 return;
129             }
130 
131             mRawWallpaperSize = dimensions;
132             setUpExploreIntent(ImagePreviewFragment.this::initFullResView);
133         });
134 
135         setUpLoadingIndicator();
136 
137         return view;
138     }
139 
140     @Override
setUpBottomSheetView(ViewGroup bottomSheet)141     protected void setUpBottomSheetView(ViewGroup bottomSheet) {
142         // Nothing needed here.
143     }
144 
145     @Override
isLoaded()146     protected boolean isLoaded() {
147         return mFullResImageView != null && mFullResImageView.hasImage();
148     }
149 
150     @Override
onClickOk()151     public void onClickOk() {
152         FragmentActivity activity = getActivity();
153         if (activity != null) {
154             activity.finish();
155         }
156     }
157 
158     @Override
onDestroy()159     public void onDestroy() {
160         super.onDestroy();
161         if (mLoadingProgressBar != null) {
162             mLoadingProgressBar.hide();
163         }
164         mFullResImageView.recycle();
165     }
166 
167     @Override
onSaveInstanceState(Bundle outState)168     public void onSaveInstanceState(Bundle outState) {
169         super.onSaveInstanceState(outState);
170 
171         final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
172         outState.putInt(KEY_BOTTOM_SHEET_STATE, bottomSheetBehavior.getState());
173     }
174 
175     @Override
setBottomSheetContentAlpha(float alpha)176     protected void setBottomSheetContentAlpha(float alpha) {
177         mInfoPageController.setContentAlpha(alpha);
178     }
179 
180     @Override
getExploreButtonLabel(Context context)181     protected CharSequence getExploreButtonLabel(Context context) {
182         return context.getString(mWallpaper.getActionLabelRes(context));
183     }
184 
185     /**
186      * Initializes MosaicView by initializing tiling, setting a fallback page bitmap, and
187      * initializing a zoom-scroll observer and click listener.
188      */
initFullResView()189     private void initFullResView() {
190         mFullResImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP);
191 
192         // Set a solid black "page bitmap" so MosaicView draws a black background while waiting
193         // for the image to load or a transparent one if a thumbnail already loaded.
194         Bitmap blackBitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888);
195         int color = (mLowResImageView.getDrawable() == null) ? Color.BLACK : Color.TRANSPARENT;
196         blackBitmap.setPixel(0, 0, color);
197         mFullResImageView.setImage(ImageSource.bitmap(blackBitmap));
198 
199         // Then set a fallback "page bitmap" to cover the whole MosaicView, which is an actual
200         // (lower res) version of the image to be displayed.
201         Point targetPageBitmapSize = new Point(mRawWallpaperSize);
202         mWallpaperAsset.decodeBitmap(targetPageBitmapSize.x, targetPageBitmapSize.y,
203                 pageBitmap -> {
204                     // Check that the activity is still around since the decoding task started.
205                     if (getActivity() == null) {
206                         return;
207                     }
208 
209                     // Some of these may be null depending on if the Fragment is paused, stopped,
210                     // or destroyed.
211                     if (mLoadingProgressBar != null) {
212                         mLoadingProgressBar.hide();
213                     }
214                     // The page bitmap may be null if there was a decoding error, so show an
215                     // error dialog.
216                     if (pageBitmap == null) {
217                         showLoadWallpaperErrorDialog();
218                         return;
219                     }
220                     if (mFullResImageView != null) {
221                         // Set page bitmap.
222                         mFullResImageView.setImage(ImageSource.bitmap(pageBitmap));
223 
224                         setDefaultWallpaperZoomAndScroll();
225                         crossFadeInMosaicView();
226                     }
227                     getActivity().invalidateOptionsMenu();
228 
229                     populateInfoPage(mInfoPageController);
230                 });
231     }
232 
233     /**
234      * Makes the MosaicView visible with an alpha fade-in animation while fading out the loading
235      * indicator.
236      */
crossFadeInMosaicView()237     private void crossFadeInMosaicView() {
238         long shortAnimationDuration = getResources().getInteger(
239                 android.R.integer.config_shortAnimTime);
240 
241         mFullResImageView.setAlpha(0f);
242         mFullResImageView.animate()
243                 .alpha(1f)
244                 .setDuration(shortAnimationDuration)
245                 .setListener(new AnimatorListenerAdapter() {
246                     @Override
247                     public void onAnimationEnd(Animator animation) {
248                         // Clear the thumbnail bitmap reference to save memory since it's no longer
249                         // visible.
250                         if (mLowResImageView != null) {
251                             mLowResImageView.setImageBitmap(null);
252                         }
253                     }
254                 });
255 
256         mLoadingProgressBar.animate()
257                 .alpha(0f)
258                 .setDuration(shortAnimationDuration)
259                 .setListener(new AnimatorListenerAdapter() {
260                     @Override
261                     public void onAnimationEnd(Animator animation) {
262                         if (mLoadingProgressBar != null) {
263                             mLoadingProgressBar.hide();
264                         }
265                     }
266                 });
267     }
268 
269     /**
270      * Sets the default wallpaper zoom and scroll position based on a "crop surface"
271      * (with extra width to account for parallax) superimposed on the screen. Shows as much of the
272      * wallpaper as possible on the crop surface and align screen to crop surface such that the
273      * default preview matches what would be seen by the user in the left-most home screen.
274      *
275      * <p>This method is called once in the Fragment lifecycle after the wallpaper asset has loaded
276      * and rendered to the layout.
277      */
setDefaultWallpaperZoomAndScroll()278     private void setDefaultWallpaperZoomAndScroll() {
279         // Determine minimum zoom to fit maximum visible area of wallpaper on crop surface.
280         float defaultWallpaperZoom =
281                 WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mDefaultCropSurfaceSize);
282         float minWallpaperZoom =
283                 WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mScreenSize);
284 
285         Point screenToCropSurfacePosition = WallpaperCropUtils.calculateCenterPosition(
286                 mDefaultCropSurfaceSize, mScreenSize, true /* alignStart */, isRtl());
287         Point zoomedWallpaperSize = new Point(
288                 Math.round(mRawWallpaperSize.x * defaultWallpaperZoom),
289                 Math.round(mRawWallpaperSize.y * defaultWallpaperZoom));
290         Point cropSurfaceToWallpaperPosition = WallpaperCropUtils.calculateCenterPosition(
291                 zoomedWallpaperSize, mDefaultCropSurfaceSize, false /* alignStart */, isRtl());
292 
293         // Set min wallpaper zoom and max zoom on MosaicView widget.
294         mFullResImageView.setMaxScale(Math.max(DEFAULT_WALLPAPER_MAX_ZOOM, defaultWallpaperZoom));
295         mFullResImageView.setMinScale(minWallpaperZoom);
296 
297         // Set center to composite positioning between scaled wallpaper and screen.
298         PointF centerPosition = new PointF(
299                 mRawWallpaperSize.x / 2f,
300                 mRawWallpaperSize.y / 2f);
301         centerPosition.offset(-(screenToCropSurfacePosition.x + cropSurfaceToWallpaperPosition.x),
302                 -(screenToCropSurfacePosition.y + cropSurfaceToWallpaperPosition.y));
303 
304         mFullResImageView.setScaleAndCenter(minWallpaperZoom, centerPosition);
305     }
306 
calculateCropRect()307     private Rect calculateCropRect() {
308         // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom).
309         float wallpaperZoom = mFullResImageView.getScale();
310         int scaledWallpaperWidth = (int) (mRawWallpaperSize.x * wallpaperZoom);
311         int scaledWallpaperHeight = (int) (mRawWallpaperSize.y * wallpaperZoom);
312         Rect rect = new Rect();
313         mFullResImageView.visibleFileRect(rect);
314         int scrollX = (int) (rect.left * wallpaperZoom);
315         int scrollY = (int) (rect.top * wallpaperZoom);
316 
317         rect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight);
318 
319         Display defaultDisplay =  requireActivity().getWindowManager().getDefaultDisplay();
320         Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay);
321         // Crop rect should start off as the visible screen and then include extra width and height
322         // if available within wallpaper at the current zoom.
323         Rect cropRect = new Rect(scrollX, scrollY, scrollX + screenSize.x, scrollY + screenSize.y);
324 
325         Point defaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
326                 getResources(), defaultDisplay);
327         int extraWidth = defaultCropSurfaceSize.x - screenSize.x;
328         int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - screenSize.y) / 2f);
329 
330         // Try to increase size of screenRect to include extra width depending on the layout
331         // direction.
332         if (isRtl()) {
333             cropRect.left = Math.max(cropRect.left - extraWidth, rect.left);
334         } else {
335             cropRect.right = Math.min(cropRect.right + extraWidth, rect.right);
336         }
337 
338         // Try to increase the size of the cropRect to to include extra height.
339         int availableExtraHeightTop = cropRect.top - Math.max(
340                 rect.top,
341                 cropRect.top - extraHeightTopAndBottom);
342         int availableExtraHeightBottom = Math.min(
343                 rect.bottom,
344                 cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom;
345 
346         int availableExtraHeightTopAndBottom =
347                 Math.min(availableExtraHeightTop, availableExtraHeightBottom);
348         cropRect.top -= availableExtraHeightTopAndBottom;
349         cropRect.bottom += availableExtraHeightTopAndBottom;
350 
351         return cropRect;
352     }
353 
354     @Override
setCurrentWallpaper(@estination int destination)355     protected void setCurrentWallpaper(@Destination int destination) {
356         mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, mWallpaperAsset,
357                 destination, mFullResImageView.getScale(), calculateCropRect(),
358                 new SetWallpaperCallback() {
359                     @Override
360                     public void onSuccess() {
361                         finishActivityWithResultOk();
362                     }
363 
364                     @Override
365                     public void onError(@Nullable Throwable throwable) {
366                         showSetWallpaperErrorDialog(destination);
367                     }
368                 });
369     }
370 }
371