| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.wallpaper.picker; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.annotation.TargetApi; |
| import android.app.Activity; |
| import android.app.ProgressDialog; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Configuration; |
| import android.content.res.Resources.NotFoundException; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.Config; |
| import android.graphics.Color; |
| import android.graphics.Point; |
| import android.graphics.PointF; |
| import android.graphics.PorterDuff.Mode; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.support.annotation.IntDef; |
| import android.support.design.widget.BottomSheetBehavior; |
| import android.support.design.widget.BottomSheetBehavior.State; |
| import android.support.v4.app.DialogFragment; |
| import android.support.v4.app.Fragment; |
| import android.support.v4.app.FragmentActivity; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v7.app.AppCompatActivity; |
| import android.support.v7.widget.Toolbar; |
| import android.util.Log; |
| import android.view.Display; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.widget.Button; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import com.android.wallpaper.R; |
| import com.android.wallpaper.asset.Asset; |
| import com.android.wallpaper.asset.Asset.BitmapReceiver; |
| import com.android.wallpaper.asset.Asset.DimensionsReceiver; |
| import com.android.wallpaper.compat.BuildCompat; |
| import com.android.wallpaper.compat.ButtonDrawableSetterCompat; |
| import com.android.wallpaper.config.Flags; |
| import com.android.wallpaper.model.LiveWallpaperInfo; |
| import com.android.wallpaper.model.WallpaperInfo; |
| import com.android.wallpaper.module.CurrentWallpaperInfoFactory; |
| import com.android.wallpaper.module.ExploreIntentChecker; |
| import com.android.wallpaper.module.Injector; |
| import com.android.wallpaper.module.InjectorProvider; |
| import com.android.wallpaper.module.UserEventLogger; |
| import com.android.wallpaper.module.UserEventLogger.WallpaperSetFailureReason; |
| import com.android.wallpaper.module.WallpaperPersister; |
| import com.android.wallpaper.module.WallpaperPersister.Destination; |
| import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback; |
| import com.android.wallpaper.module.WallpaperPreferences; |
| import com.android.wallpaper.util.ScreenSizeCalculator; |
| import com.android.wallpaper.util.ThrowableAnalyzer; |
| import com.android.wallpaper.util.WallpaperCropUtils; |
| import com.android.wallpaper.widget.MaterialProgressDrawable; |
| import com.bumptech.glide.Glide; |
| import com.bumptech.glide.MemoryCategory; |
| import com.davemorrissey.labs.subscaleview.ImageSource; |
| import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; |
| |
| import java.util.Date; |
| import java.util.List; |
| |
| /** |
| * Fragment which displays the UI for previewing an individual wallpaper and its attribution |
| * information. |
| */ |
| public class PreviewFragment extends Fragment implements |
| SetWallpaperDialogFragment.Listener, SetWallpaperErrorDialogFragment.Listener, |
| LoadWallpaperErrorDialogFragment.Listener { |
| |
| /** |
| * User can view wallpaper and attributions in full screen, but "Set wallpaper" button is hidden. |
| */ |
| public static final int MODE_VIEW_ONLY = 0; |
| |
| /** |
| * User can view wallpaper and attributions in full screen and click "Set wallpaper" to set the |
| * wallpaper with pan and crop position to the device. |
| */ |
| public static final int MODE_CROP_AND_SET_WALLPAPER = 1; |
| |
| /** |
| * Possible preview modes for the fragment. |
| */ |
| @IntDef({ |
| MODE_VIEW_ONLY, |
| MODE_CROP_AND_SET_WALLPAPER}) |
| public @interface PreviewMode { |
| } |
| |
| protected static final String ARG_WALLPAPER = "wallpaper"; |
| protected static final String ARG_PREVIEW_MODE = "preview_mode"; |
| protected static final String ARG_TESTING_MODE_ENABLED = "testing_mode_enabled"; |
| private static final String TAG_LOAD_WALLPAPER_ERROR_DIALOG_FRAGMENT = |
| "load_wallpaper_error_dialog"; |
| private static final String TAG_SET_WALLPAPER_DIALOG_FRAGMENT = "set_wallpaper_dialog"; |
| private static final String TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT = |
| "set_wallpaper_error_dialog"; |
| private static final int UNUSED_REQUEST_CODE = 1; |
| private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f; |
| private static final String TAG = "PreviewFragment"; |
| private static final String PROGRESS_DIALOG_NO_TITLE = null; |
| private static final boolean PROGRESS_DIALOG_INDETERMINATE = true; |
| private static final float PAGE_BITMAP_MAX_HEAP_RATIO = 0.25f; |
| private static final String KEY_BOTTOM_SHEET_STATE = "key_bottom_sheet_state"; |
| |
| @PreviewMode |
| private int mPreviewMode; |
| |
| /** |
| * When true, enables a test mode of operation -- in which certain UI features are disabled to |
| * allow for UI tests to run correctly. Works around issue in ProgressDialog currently where the |
| * dialog constantly keeps the UI thread alive and blocks a test forever. |
| */ |
| private boolean mTestingModeEnabled; |
| |
| protected SubsamplingScaleImageView mFullResImageView; |
| protected WallpaperInfo mWallpaper; |
| private Asset mWallpaperAsset; |
| private WallpaperPersister mWallpaperPersister; |
| private WallpaperPreferences mPreferences; |
| private UserEventLogger mUserEventLogger; |
| private LinearLayout mBottomSheet; |
| private TextView mAttributionTitle; |
| private TextView mAttributionSubtitle1; |
| private TextView mAttributionSubtitle2; |
| private FrameLayout mAttributionExploreSection; |
| private Button mAttributionExploreButton; |
| private ImageView mPreviewPaneArrow; |
| private int mCurrentScreenOrientation; |
| private ProgressDialog mProgressDialog; |
| private Point mDefaultCropSurfaceSize; |
| private Point mScreenSize; |
| private Point mRawWallpaperSize; // Native size of wallpaper image. |
| private ImageView mLoadingIndicator; |
| private MaterialProgressDrawable mProgressDrawable; |
| private ImageView mLowResImageView; |
| |
| @SuppressWarnings("RestrictTo") |
| @State |
| private int mBottomSheetInitialState; |
| |
| private Intent mExploreIntent; |
| |
| /** |
| * Staged error dialog fragments that were unable to be shown when the hosting activity didn't |
| * allow committing fragment transactions. |
| */ |
| private SetWallpaperErrorDialogFragment mStagedSetWallpaperErrorDialogFragment; |
| private LoadWallpaperErrorDialogFragment mStagedLoadWallpaperErrorDialogFragment; |
| |
| /** |
| * Creates and returns new instance of {@link PreviewFragment} with the provided wallpaper set as |
| * an argument. |
| */ |
| public static PreviewFragment newInstance( |
| WallpaperInfo wallpaperInfo, @PreviewMode int mode, boolean testingModeEnabled) { |
| Bundle args = new Bundle(); |
| args.putParcelable(ARG_WALLPAPER, wallpaperInfo); |
| args.putInt(ARG_PREVIEW_MODE, mode); |
| args.putBoolean(ARG_TESTING_MODE_ENABLED, testingModeEnabled); |
| |
| PreviewFragment fragment = new PreviewFragment(); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| Activity activity = getActivity(); |
| Context appContext = activity.getApplicationContext(); |
| Injector injector = InjectorProvider.getInjector(); |
| |
| mWallpaperPersister = injector.getWallpaperPersister(appContext); |
| mPreferences = injector.getPreferences(appContext); |
| mUserEventLogger = injector.getUserEventLogger(appContext); |
| mWallpaper = getArguments().getParcelable(ARG_WALLPAPER); |
| mWallpaperAsset = mWallpaper.getAsset(appContext); |
| //noinspection ResourceType |
| mPreviewMode = getArguments().getInt(ARG_PREVIEW_MODE); |
| mTestingModeEnabled = getArguments().getBoolean(ARG_TESTING_MODE_ENABLED); |
| |
| setHasOptionsMenu(true); |
| |
| // Allow the layout to draw fullscreen even behind the status bar, so we can set as the status |
| // bar color a color that has a custom translucency in the theme. |
| Window window = activity.getWindow(); |
| window.getDecorView().setSystemUiVisibility( |
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
| | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); |
| |
| List<String> attributions = mWallpaper.getAttributions(activity); |
| if (attributions.size() > 0 && attributions.get(0) != null) { |
| activity.setTitle(attributions.get(0)); |
| } |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| View view = inflater.inflate(R.layout.fragment_preview, container, false); |
| |
| // Set toolbar as the action bar. |
| Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar); |
| ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); |
| ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); |
| ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayShowTitleEnabled(false); |
| |
| // Use updated fancy arrow icon for O+. |
| if (BuildCompat.isAtLeastO()) { |
| Drawable navigationIcon = getResources().getDrawable( |
| R.drawable.material_ic_arrow_back_black_24); |
| |
| // This Drawable's state is shared across the app, so make a copy of it before applying a |
| // color tint as not to affect other clients elsewhere in the app. |
| navigationIcon = navigationIcon.getConstantState().newDrawable().mutate(); |
| navigationIcon.setColorFilter( |
| getResources().getColor(R.color.material_white_100), Mode.SRC_IN); |
| navigationIcon.setAutoMirrored(true); |
| toolbar.setNavigationIcon(navigationIcon); |
| } |
| |
| ViewCompat.setPaddingRelative(toolbar, |
| /* start */ getResources().getDimensionPixelSize( |
| R.dimen.preview_toolbar_up_button_start_padding), |
| /* top */ 0, |
| /* end */ getResources().getDimensionPixelSize( |
| R.dimen.preview_toolbar_set_wallpaper_button_end_padding), |
| /* bottom */ 0); |
| |
| mFullResImageView = view.findViewById(R.id.full_res_image); |
| mLoadingIndicator = (ImageView) view.findViewById(R.id.loading_indicator); |
| |
| mBottomSheet = (LinearLayout) view.findViewById(R.id.bottom_sheet); |
| mAttributionTitle = (TextView) view.findViewById(R.id.preview_attribution_pane_title); |
| mAttributionSubtitle1 = (TextView) view.findViewById(R.id.preview_attribution_pane_subtitle1); |
| mAttributionSubtitle2 = (TextView) view.findViewById(R.id.preview_attribution_pane_subtitle2); |
| mAttributionExploreSection = (FrameLayout) view.findViewById( |
| R.id.preview_attribution_pane_explore_section); |
| mAttributionExploreButton = (Button) view.findViewById( |
| R.id.preview_attribution_pane_explore_button); |
| mPreviewPaneArrow = (ImageView) view.findViewById(R.id.preview_attribution_pane_arrow); |
| mLowResImageView = (ImageView) view.findViewById(R.id.low_res_image); |
| |
| mPreviewPaneArrow.setColorFilter( |
| getResources().getColor(R.color.preview_pane_arrow_color), Mode.SRC_IN); |
| |
| // Trim some memory from Glide to make room for the full-size image in this fragment. |
| Glide.get(getActivity()).setMemoryCategory(MemoryCategory.LOW); |
| |
| mDefaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize( |
| getResources(), getActivity().getWindowManager().getDefaultDisplay()); |
| mScreenSize = ScreenSizeCalculator.getInstance().getScreenSize( |
| getActivity().getWindowManager().getDefaultDisplay()); |
| |
| // Load a low-res placeholder image if there's a thumbnail available from the asset that can be |
| // shown to the user more quickly than the full-sized image. |
| if (mWallpaperAsset.hasLowResDataSource()) { |
| mWallpaperAsset.loadLowResDrawable(getActivity(), mLowResImageView, Color.BLACK, |
| new WallpaperPreviewBitmapTransformation(getActivity().getApplicationContext(), isRtl())); |
| } |
| |
| mWallpaperAsset.decodeRawDimensions(getActivity(), new DimensionsReceiver() { |
| @Override |
| public void onDimensionsDecoded(Point dimensions) { |
| // Don't continue loading the wallpaper if the Fragment is detached. |
| Activity activity = getActivity(); |
| if (activity == null) { |
| return; |
| } |
| |
| // Return early and show a dialog if dimensions are null (signaling a decoding error). |
| if (dimensions == null) { |
| showLoadWallpaperErrorDialog(); |
| return; |
| } |
| |
| mRawWallpaperSize = dimensions; |
| ExploreIntentChecker intentChecker = |
| InjectorProvider.getInjector().getExploreIntentChecker(activity); |
| String actionUrl = mWallpaper.getActionUrl(activity); |
| if (actionUrl != null && !actionUrl.isEmpty()) { |
| Uri exploreUri = Uri.parse(mWallpaper.getActionUrl(activity)); |
| |
| intentChecker.fetchValidActionViewIntent(exploreUri, exploreIntent -> { |
| if (getActivity() == null) { |
| return; |
| } |
| |
| mExploreIntent = exploreIntent; |
| initFullResView(); |
| }); |
| } else { |
| initFullResView(); |
| } |
| } |
| }); |
| |
| // Configure loading indicator with a MaterialProgressDrawable. |
| mProgressDrawable = |
| new MaterialProgressDrawable(getActivity().getApplicationContext(), mLoadingIndicator); |
| mProgressDrawable.setAlpha(255); |
| mProgressDrawable.setBackgroundColor(getResources().getColor(R.color.material_white_100)); |
| mProgressDrawable.setColorSchemeColors(getResources().getColor(R.color.accent_color)); |
| mProgressDrawable.updateSizes(MaterialProgressDrawable.LARGE); |
| mLoadingIndicator.setImageDrawable(mProgressDrawable); |
| |
| // We don't want to show the spinner every time we load an image if it loads quickly; instead, |
| // only start showing the spinner if loading the image has taken longer than half of a second. |
| mLoadingIndicator.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| if (mFullResImageView != null && !mFullResImageView.hasImage() |
| && !mTestingModeEnabled) { |
| mLoadingIndicator.setVisibility(View.VISIBLE); |
| mLoadingIndicator.setAlpha(1f); |
| if (mProgressDrawable != null) { |
| mProgressDrawable.start(); |
| } |
| } |
| } |
| }, 500); |
| |
| mBottomSheetInitialState = (savedInstanceState == null) |
| ? BottomSheetBehavior.STATE_EXPANDED |
| : savedInstanceState.getInt(KEY_BOTTOM_SHEET_STATE, |
| BottomSheetBehavior.STATE_EXPANDED); |
| setUpBottomSheetListeners(); |
| |
| return view; |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| |
| WallpaperPreferences preferences = InjectorProvider.getInjector().getPreferences(getActivity()); |
| preferences.setLastAppActiveTimestamp(new Date().getTime()); |
| |
| // Show the staged 'load wallpaper' or 'set wallpaper' error dialog fragments if there is one |
| // that was unable to be shown earlier when this fragment's hosting activity didn't allow |
| // committing fragment transactions. |
| if (mStagedLoadWallpaperErrorDialogFragment != null) { |
| mStagedLoadWallpaperErrorDialogFragment.show( |
| getFragmentManager(), TAG_LOAD_WALLPAPER_ERROR_DIALOG_FRAGMENT); |
| mStagedLoadWallpaperErrorDialogFragment = null; |
| } |
| if (mStagedSetWallpaperErrorDialogFragment != null) { |
| mStagedSetWallpaperErrorDialogFragment.show( |
| getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT); |
| mStagedSetWallpaperErrorDialogFragment = null; |
| } |
| } |
| |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| super.onCreateOptionsMenu(menu, inflater); |
| inflater.inflate(R.menu.preview_menu, menu); |
| } |
| |
| @Override |
| public void onPrepareOptionsMenu(Menu menu) { |
| super.onPrepareOptionsMenu(menu); |
| MenuItem setWallpaperButton = menu.findItem(R.id.set_wallpaper); |
| |
| if (mPreviewMode == MODE_CROP_AND_SET_WALLPAPER && isWallpaperLoaded()) { |
| setWallpaperButton.setVisible(true); |
| } else { |
| setWallpaperButton.setVisible(false); |
| } |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| int id = item.getItemId(); |
| if (id == R.id.set_wallpaper) { |
| if (BuildCompat.isAtLeastN()) { |
| requestDestination(); |
| } else { |
| setCurrentWallpaper(WallpaperPersister.DEST_HOME_SCREEN); |
| } |
| return true; |
| } else if (id == android.R.id.home) { |
| // The Preview screen has multiple entry points. It could be opened from either |
| // the IndividualPreviewActivity, the "My photos" selection (by way of |
| // TopLevelPickerActivity), or from a system "crop and set wallpaper" intent. |
| // Therefore, handle the Up button as a global Back. |
| getActivity().onBackPressed(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void requestDestination() { |
| CurrentWallpaperInfoFactory factory = InjectorProvider.getInjector() |
| .getCurrentWallpaperFactory(getContext()); |
| |
| factory.createCurrentWallpaperInfos((homeWallpaper, lockWallpaper, presentationMode) -> { |
| SetWallpaperDialogFragment setWallpaperDialog = new SetWallpaperDialogFragment(); |
| setWallpaperDialog.setTargetFragment(this, UNUSED_REQUEST_CODE); |
| if (homeWallpaper instanceof LiveWallpaperInfo && lockWallpaper == null) { |
| // if the lock wallpaper is a live wallpaper, we cannot set a home-only static one |
| setWallpaperDialog.setHomeOptionAvailable(false); |
| } |
| setWallpaperDialog.show(getFragmentManager(), TAG_SET_WALLPAPER_DIALOG_FRAGMENT); |
| }, true); // Force refresh as the wallpaper may have been set while this fragment was paused |
| } |
| |
| @Override |
| public void onSetHomeScreen() { |
| setCurrentWallpaper(WallpaperPersister.DEST_HOME_SCREEN); |
| } |
| |
| @Override |
| public void onSetLockScreen() { |
| setCurrentWallpaper(WallpaperPersister.DEST_LOCK_SCREEN); |
| } |
| |
| @Override |
| public void onSetBoth() { |
| setCurrentWallpaper(WallpaperPersister.DEST_BOTH); |
| } |
| |
| @Override |
| public void onClickTryAgain(@Destination int wallpaperDestination) { |
| setCurrentWallpaper(wallpaperDestination); |
| } |
| |
| @Override |
| public void onClickOk() { |
| FragmentActivity activity = getActivity(); |
| if (activity != null) { |
| activity.finish(); |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| if (mProgressDialog != null) { |
| mProgressDialog.dismiss(); |
| } |
| if (mProgressDrawable != null) { |
| mProgressDrawable.stop(); |
| } |
| mFullResImageView.recycle(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| |
| final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet); |
| outState.putInt(KEY_BOTTOM_SHEET_STATE, bottomSheetBehavior.getState()); |
| } |
| |
| private void updatePreviewPaneArrow(int bottomSheetState) { |
| if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) { |
| mPreviewPaneArrow.setImageResource(R.drawable.material_ic_keyboard_arrow_up_black_24); |
| mPreviewPaneArrow.setContentDescription( |
| getResources().getString(R.string.expand_attribution_panel)); |
| } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) { |
| mPreviewPaneArrow.setImageResource(R.drawable.material_ic_keyboard_arrow_down_black_24); |
| mPreviewPaneArrow.setContentDescription( |
| getResources().getString(R.string.collapse_attribution_panel)); |
| } |
| mPreviewPaneArrow.setColorFilter( |
| getResources().getColor(R.color.preview_pane_arrow_color), Mode.SRC_IN); |
| } |
| |
| /** |
| * Returns a zoom level that is similar to the actual zoom, but that is exactly 0.5 ** n for some |
| * integer n. This is useful for downsampling a bitmap--we want to see the bitmap at full detail, |
| * or downsampled to 1 in every 2 pixels, or 1 in 4, and so on, depending on the zoom. |
| */ |
| private static float getDownsampleZoom(float actualZoom) { |
| if (actualZoom > 1) { |
| // Very zoomed in, but we can't sample more than 1 pixel per pixel. |
| return 1.0f; |
| } |
| float lower = 1.0f / roundUpToPower2((int) Math.ceil(1 / actualZoom)); |
| float upper = lower * 2; |
| return nearestValue(actualZoom, lower, upper); |
| } |
| |
| /** |
| * Returns the integer rounded up to the next power of 2. |
| */ |
| private static int roundUpToPower2(int value) { |
| return 1 << (32 - Integer.numberOfLeadingZeros(value - 1)); |
| } |
| |
| /** |
| * Returns the closer of two values a and b to the given value. |
| */ |
| private static float nearestValue(float value, float a, float b) { |
| return Math.abs(a - value) < Math.abs(b - value) ? a : b; |
| } |
| |
| private void setUpBottomSheetListeners() { |
| final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet); |
| |
| OnClickListener onClickListener = new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) { |
| bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); |
| } else if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { |
| bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); |
| } |
| } |
| }; |
| mAttributionTitle.setOnClickListener(onClickListener); |
| mPreviewPaneArrow.setOnClickListener(onClickListener); |
| |
| bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { |
| @Override |
| public void onStateChanged(View bottomSheet, int newState) { |
| // Don't respond to lingering state change events occurring after the fragment has already |
| // been detached from the activity. Else, IllegalStateException may occur when trying to |
| // fetch resources. |
| if (getActivity() == null) { |
| return; |
| } |
| |
| updatePreviewPaneArrow(newState); |
| } |
| |
| @Override |
| public void onSlide(View bottomSheet, float slideOffset) { |
| float alpha; |
| if (slideOffset >= 0) { |
| alpha = slideOffset; |
| } else { |
| alpha = 1f - slideOffset; |
| } |
| mAttributionTitle.setAlpha(alpha); |
| mAttributionSubtitle1.setAlpha(alpha); |
| mAttributionSubtitle2.setAlpha(alpha); |
| mAttributionExploreButton.setAlpha(alpha); |
| } |
| }); |
| } |
| |
| private boolean isWallpaperLoaded() { |
| return mFullResImageView.hasImage(); |
| } |
| |
| private void populateAttributionPane() { |
| final Context context = getContext(); |
| |
| final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet); |
| |
| List<String> attributions = mWallpaper.getAttributions(context); |
| if (attributions.size() > 0 && attributions.get(0) != null) { |
| mAttributionTitle.setText(attributions.get(0)); |
| } |
| |
| if (attributions.size() > 1 && attributions.get(1) != null) { |
| mAttributionSubtitle1.setVisibility(View.VISIBLE); |
| mAttributionSubtitle1.setText(attributions.get(1)); |
| } |
| |
| if (attributions.size() > 2 && attributions.get(2) != null) { |
| mAttributionSubtitle2.setVisibility(View.VISIBLE); |
| mAttributionSubtitle2.setText(attributions.get(2)); |
| } |
| |
| String actionUrl = mWallpaper.getActionUrl(context); |
| if (actionUrl != null && !actionUrl.isEmpty()) { |
| if (mExploreIntent != null) { |
| if (Flags.skipDailyWallpaperButtonEnabled) { |
| Drawable exploreButtonDrawable = context.getDrawable( |
| mWallpaper.getActionIconRes(context)); |
| |
| // This Drawable's state is shared across the app, so make a copy of it before applying a |
| // color tint as not to affect other clients elsewhere in the app. |
| exploreButtonDrawable = exploreButtonDrawable.getConstantState() |
| .newDrawable().mutate(); |
| // Color the "compass" icon with the accent color. |
| exploreButtonDrawable.setColorFilter( |
| getResources().getColor(R.color.accent_color), Mode.SRC_IN); |
| ButtonDrawableSetterCompat.setDrawableToButtonStart( |
| mAttributionExploreButton, exploreButtonDrawable); |
| mAttributionExploreButton.setText(context.getString( |
| mWallpaper.getActionLabelRes(context))); |
| } |
| |
| mAttributionExploreSection.setVisibility(View.VISIBLE); |
| mAttributionExploreButton.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| mUserEventLogger.logActionClicked(mWallpaper.getCollectionId(context), |
| mWallpaper.getActionLabelRes(context)); |
| |
| startActivity(mExploreIntent); |
| } |
| }); |
| } |
| } |
| |
| mBottomSheet.setVisibility(View.VISIBLE); |
| |
| // Initialize the state of the BottomSheet based on the current state because if the initial |
| // and current state are the same, the state change listener won't fire and set the correct |
| // arrow asset and text alpha. |
| if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { |
| updatePreviewPaneArrow(BottomSheetBehavior.STATE_EXPANDED); |
| } else { |
| mAttributionTitle.setAlpha(0f); |
| mAttributionSubtitle1.setAlpha(0f); |
| mAttributionSubtitle2.setAlpha(0f); |
| } |
| |
| // Let the state change listener take care of animating a state change to the initial state if |
| // there's a state change. |
| bottomSheetBehavior.setState(mBottomSheetInitialState); |
| } |
| |
| /** |
| * Initializes MosaicView by initializing tiling, setting a fallback page bitmap, and initializing |
| * a zoom-scroll observer and click listener. |
| */ |
| private void initFullResView() { |
| mFullResImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP); |
| |
| // Set a solid black "page bitmap" so MosaicView draws a black background while waiting |
| // for the image to load or a transparent one if a thumbnail already loaded. |
| Bitmap blackBitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888); |
| int color = (mLowResImageView.getDrawable() == null) ? Color.BLACK : Color.TRANSPARENT; |
| blackBitmap.setPixel(0, 0, color); |
| mFullResImageView.setImage(ImageSource.bitmap(blackBitmap)); |
| |
| // Then set a fallback "page bitmap" to cover the whole MosaicView, which is an actual |
| // (lower res) version of the image to be displayed. |
| Point targetPageBitmapSize = new Point(mRawWallpaperSize); |
| mWallpaperAsset.decodeBitmap(targetPageBitmapSize.x, targetPageBitmapSize.y, |
| new BitmapReceiver() { |
| @Override |
| public void onBitmapDecoded(Bitmap pageBitmap) { |
| // Check that the activity is still around since the decoding task started. |
| if (getActivity() == null) { |
| return; |
| } |
| |
| // Some of these may be null depending on if the Fragment is paused, stopped, |
| // or destroyed. |
| if (mLoadingIndicator != null) { |
| mLoadingIndicator.setVisibility(View.GONE); |
| } |
| // The page bitmap may be null if there was a decoding error, so show an error dialog. |
| if (pageBitmap == null) { |
| showLoadWallpaperErrorDialog(); |
| return; |
| } |
| if (mFullResImageView != null) { |
| // Set page bitmap. |
| mFullResImageView.setImage(ImageSource.bitmap(pageBitmap)); |
| |
| setDefaultWallpaperZoomAndScroll(); |
| crossFadeInMosaicView(); |
| |
| // Record memory snapshot of app one second delayed to allow time for MosaicView tiles |
| // to be decoded and overlaid on top of the page bitmap. |
| new Handler().postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| if (getActivity() == null) { |
| return; |
| } |
| |
| InjectorProvider.getInjector().getPerformanceMonitor() |
| .recordFullResPreviewLoadedMemorySnapshot(); |
| } |
| }, 1000); |
| } |
| if (mProgressDrawable != null) { |
| mProgressDrawable.stop(); |
| } |
| getActivity().invalidateOptionsMenu(); |
| |
| populateAttributionPane(); |
| } |
| }); |
| } |
| |
| /** |
| * Makes the MosaicView visible with an alpha fade-in animation while fading out the loading |
| * indicator. |
| */ |
| private void crossFadeInMosaicView() { |
| long shortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); |
| |
| mFullResImageView.setAlpha(0f); |
| mFullResImageView.animate() |
| .alpha(1f) |
| .setDuration(shortAnimationDuration) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| // Clear the thumbnail bitmap reference to save memory since it's no longer visible. |
| if (mLowResImageView != null) { |
| mLowResImageView.setImageBitmap(null); |
| } |
| } |
| }); |
| |
| mLoadingIndicator.animate() |
| .alpha(0f) |
| .setDuration(shortAnimationDuration) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (mLoadingIndicator != null) { |
| mLoadingIndicator.setVisibility(View.GONE); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Sets the default wallpaper zoom and scroll position based on a "crop surface" |
| * (with extra width to account for parallax) superimposed on the screen. Shows as much of the |
| * wallpaper as possible on the crop surface and align screen to crop surface such that the |
| * default preview matches what would be seen by the user in the left-most home screen. |
| * |
| * <p>This method is called once in the Fragment lifecycle after the wallpaper asset has loaded |
| * and rendered to the layout. |
| */ |
| private void setDefaultWallpaperZoomAndScroll() { |
| // Determine minimum zoom to fit maximum visible area of wallpaper on crop surface. |
| float defaultWallpaperZoom = |
| WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mDefaultCropSurfaceSize); |
| float minWallpaperZoom = |
| WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mScreenSize); |
| |
| Point screenToCropSurfacePosition = WallpaperCropUtils.calculateCenterPosition( |
| mDefaultCropSurfaceSize, mScreenSize, true /* alignStart */, isRtl()); |
| Point zoomedWallpaperSize = new Point( |
| Math.round(mRawWallpaperSize.x * defaultWallpaperZoom), |
| Math.round(mRawWallpaperSize.y * defaultWallpaperZoom)); |
| Point cropSurfaceToWallpaperPosition = WallpaperCropUtils.calculateCenterPosition( |
| zoomedWallpaperSize, mDefaultCropSurfaceSize, false /* alignStart */, isRtl()); |
| |
| // Set min wallpaper zoom and max zoom on MosaicView widget. |
| mFullResImageView.setMaxScale(Math.max(DEFAULT_WALLPAPER_MAX_ZOOM, defaultWallpaperZoom)); |
| mFullResImageView.setMinScale(minWallpaperZoom); |
| |
| // Set center to composite positioning between scaled wallpaper and screen. |
| PointF centerPosition = new PointF( |
| mRawWallpaperSize.x / 2f, |
| mRawWallpaperSize.y / 2f); |
| centerPosition.offset( - (screenToCropSurfacePosition.x + cropSurfaceToWallpaperPosition.x), |
| - (screenToCropSurfacePosition.y + cropSurfaceToWallpaperPosition.y)); |
| |
| mFullResImageView.setScaleAndCenter(minWallpaperZoom, centerPosition); |
| } |
| |
| protected Rect calculateCropRect() { |
| // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom). |
| float wallpaperZoom = mFullResImageView.getScale(); |
| int scaledWallpaperWidth = (int) (mRawWallpaperSize.x * wallpaperZoom); |
| int scaledWallpaperHeight = (int) (mRawWallpaperSize.y * wallpaperZoom); |
| Rect rect = new Rect(); |
| mFullResImageView.visibleFileRect(rect); |
| int scrollX = (int) (rect.left * wallpaperZoom); |
| int scrollY = (int) (rect.top * wallpaperZoom); |
| |
| rect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight); |
| Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize( |
| getActivity().getWindowManager().getDefaultDisplay()); |
| // Crop rect should start off as the visible screen and then include extra width and height if |
| // available within wallpaper at the current zoom. |
| Rect cropRect = new Rect(scrollX, scrollY, scrollX + screenSize.x, scrollY + screenSize.y); |
| |
| Point defaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize( |
| getResources(), getActivity().getWindowManager().getDefaultDisplay()); |
| int extraWidth = defaultCropSurfaceSize.x - screenSize.x; |
| int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - screenSize.y) / 2f); |
| |
| // Try to increase size of screenRect to include extra width depending on the layout direction. |
| if (isRtl()) { |
| cropRect.left = Math.max(cropRect.left - extraWidth, rect.left); |
| } else { |
| cropRect.right = Math.min(cropRect.right + extraWidth, rect.right); |
| } |
| |
| // Try to increase the size of the cropRect to to include extra height. |
| int availableExtraHeightTop = cropRect.top - Math.max( |
| rect.top, |
| cropRect.top - extraHeightTopAndBottom); |
| int availableExtraHeightBottom = Math.min( |
| rect.bottom, |
| cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom; |
| |
| int availableExtraHeightTopAndBottom = |
| Math.min(availableExtraHeightTop, availableExtraHeightBottom); |
| cropRect.top -= availableExtraHeightTopAndBottom; |
| cropRect.bottom += availableExtraHeightTopAndBottom; |
| |
| return cropRect; |
| } |
| |
| /** |
| * Sets current wallpaper to the device based on current zoom and scroll state. |
| * |
| * @param destination The wallpaper destination i.e. home vs. lockscreen vs. both. |
| */ |
| private void setCurrentWallpaper(@Destination final int destination) { |
| mPreferences.setPendingWallpaperSetStatus(WallpaperPreferences.WALLPAPER_SET_PENDING); |
| |
| // Save current screen rotation so we can temporarily disable rotation while setting the |
| // wallpaper and restore after setting the wallpaper finishes. |
| saveAndLockScreenOrientation(); |
| |
| // Clear MosaicView tiles and Glide's cache and pools to reclaim memory for final cropped |
| // bitmap. |
| Glide.get(getActivity()).clearMemory(); |
| |
| // ProgressDialog endlessly updates the UI thread, keeping it from going idle which therefore |
| // causes Espresso to hang once the dialog is shown. |
| if (!mTestingModeEnabled) { |
| int themeResId; |
| if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) { |
| themeResId = R.style.ProgressDialogThemePreL; |
| } else { |
| themeResId = R.style.LightDialogTheme; |
| } |
| mProgressDialog = new ProgressDialog(getActivity(), themeResId); |
| |
| mProgressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE); |
| mProgressDialog.setMessage( |
| getResources().getString(R.string.set_wallpaper_progress_message)); |
| mProgressDialog.setIndeterminate(PROGRESS_DIALOG_INDETERMINATE); |
| mProgressDialog.show(); |
| } |
| |
| float wallpaperScale = mFullResImageView.getScale(); |
| Rect cropRect = calculateCropRect(); |
| mWallpaperPersister.setIndividualWallpaper(mWallpaper, mWallpaperAsset, cropRect, |
| wallpaperScale, destination, new SetWallpaperCallback() { |
| @Override |
| public void onSuccess() { |
| Context context = getContext(); |
| mUserEventLogger.logWallpaperSet( |
| mWallpaper.getCollectionId(context), |
| mWallpaper.getWallpaperId()); |
| mPreferences.setPendingWallpaperSetStatus( |
| WallpaperPreferences.WALLPAPER_SET_NOT_PENDING); |
| mUserEventLogger.logWallpaperSetResult( |
| UserEventLogger.WALLPAPER_SET_RESULT_SUCCESS); |
| |
| if (getActivity() == null) { |
| return; |
| } |
| |
| if (mProgressDialog != null) { |
| mProgressDialog.dismiss(); |
| } |
| |
| restoreScreenOrientation(); |
| finishActivityWithResultOk(); |
| } |
| |
| @Override |
| public void onError(Throwable throwable) { |
| mPreferences.setPendingWallpaperSetStatus( |
| WallpaperPreferences.WALLPAPER_SET_NOT_PENDING); |
| mUserEventLogger.logWallpaperSetResult( |
| UserEventLogger.WALLPAPER_SET_RESULT_FAILURE); |
| @WallpaperSetFailureReason int failureReason = ThrowableAnalyzer.isOOM(throwable) |
| ? UserEventLogger.WALLPAPER_SET_FAILURE_REASON_OOM |
| : UserEventLogger.WALLPAPER_SET_FAILURE_REASON_OTHER; |
| mUserEventLogger.logWallpaperSetFailureReason(failureReason); |
| |
| if (getActivity() == null) { |
| return; |
| } |
| |
| if (mProgressDialog != null) { |
| mProgressDialog.dismiss(); |
| } |
| restoreScreenOrientation(); |
| showSetWallpaperErrorDialog(destination); |
| } |
| }); |
| } |
| |
| private void finishActivityWithResultOk() { |
| try { |
| Toast.makeText( |
| getActivity(), R.string.wallpaper_set_successfully_message, Toast.LENGTH_SHORT).show(); |
| } catch (NotFoundException e) { |
| Log.e(TAG, "Could not show toast " + e); |
| } |
| getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out); |
| getActivity().setResult(Activity.RESULT_OK); |
| getActivity().finish(); |
| } |
| |
| private void showSetWallpaperErrorDialog(@Destination int wallpaperDestination) { |
| SetWallpaperErrorDialogFragment newFragment = SetWallpaperErrorDialogFragment.newInstance( |
| R.string.set_wallpaper_error_message, wallpaperDestination); |
| newFragment.setTargetFragment(this, UNUSED_REQUEST_CODE); |
| |
| // Show 'set wallpaper' error dialog now if it's safe to commit fragment transactions, otherwise |
| // stage it for later when the hosting activity is in a state to commit fragment transactions. |
| BasePreviewActivity activity = (BasePreviewActivity) getActivity(); |
| if (activity.isSafeToCommitFragmentTransaction()) { |
| newFragment.show(getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT); |
| } else { |
| mStagedSetWallpaperErrorDialogFragment = newFragment; |
| } |
| } |
| |
| /** |
| * Shows 'load wallpaper' error dialog now or stage it to be shown when the hosting activity is in |
| * a state that allows committing fragment transactions. |
| */ |
| private void showLoadWallpaperErrorDialog() { |
| LoadWallpaperErrorDialogFragment dialogFragment = |
| LoadWallpaperErrorDialogFragment.newInstance(); |
| dialogFragment.setTargetFragment(PreviewFragment.this, UNUSED_REQUEST_CODE); |
| |
| // Show 'load wallpaper' error dialog now or stage it to be shown when the hosting |
| // activity is in a state that allows committing fragment transactions. |
| BasePreviewActivity activity = (BasePreviewActivity) getActivity(); |
| if (activity != null && activity.isSafeToCommitFragmentTransaction()) { |
| dialogFragment.show(PreviewFragment.this.getFragmentManager(), |
| TAG_LOAD_WALLPAPER_ERROR_DIALOG_FRAGMENT); |
| } else { |
| mStagedLoadWallpaperErrorDialogFragment = dialogFragment; |
| } |
| } |
| |
| @IntDef({ |
| ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, |
| ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, |
| ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, |
| ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE}) |
| private @interface ActivityInfoScreenOrientation { |
| } |
| |
| /** |
| * Gets the appropriate ActivityInfo orientation for the current configuration orientation to |
| * enable locking screen rotation at API levels lower than 18. |
| */ |
| @ActivityInfoScreenOrientation |
| private int getCompatActivityInfoOrientation() { |
| int configOrientation = getResources().getConfiguration().orientation; |
| final Display display = getActivity().getWindowManager().getDefaultDisplay(); |
| int naturalOrientation = Configuration.ORIENTATION_LANDSCAPE; |
| switch (display.getRotation()) { |
| case Surface.ROTATION_0: |
| case Surface.ROTATION_180: |
| // We are currently in the same basic orientation as the natural orientation. |
| naturalOrientation = configOrientation; |
| break; |
| case Surface.ROTATION_90: |
| case Surface.ROTATION_270: |
| // We are currently in the other basic orientation to the natural orientation. |
| naturalOrientation = (configOrientation == Configuration.ORIENTATION_LANDSCAPE) |
| ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE; |
| break; |
| default: |
| // continue below |
| } |
| |
| // Since the map starts at portrait, we need to offset if this device's natural orientation |
| // is landscape. |
| int indexOffset = 0; |
| if (naturalOrientation == Configuration.ORIENTATION_LANDSCAPE) { |
| indexOffset = 1; |
| } |
| |
| switch ((display.getRotation() + indexOffset) % 4) { |
| case 0: |
| return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; |
| case 1: |
| return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; |
| case 2: |
| return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; |
| case 3: |
| return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; |
| default: |
| Log.e(TAG, "Display rotation did not correspond to a valid ActivityInfo orientation with" |
| + " display rotation: " + display.getRotation() + " and index offset: " + indexOffset |
| + "."); |
| return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; |
| } |
| } |
| |
| @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) |
| private void saveAndLockScreenOrientation() { |
| mCurrentScreenOrientation = getActivity().getRequestedOrientation(); |
| if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) { |
| getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); |
| } else { |
| getActivity().setRequestedOrientation(getCompatActivityInfoOrientation()); |
| } |
| } |
| |
| private void restoreScreenOrientation() { |
| getActivity().setRequestedOrientation(mCurrentScreenOrientation); |
| } |
| |
| /** |
| * Returns whether layout direction is RTL (or false for LTR). Since native RTL layout support was |
| * added in API 17, returns false for versions lower than 17. |
| */ |
| private boolean isRtl() { |
| return VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 |
| && getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; |
| } |
| } |