blob: b0a7d271102eeaf38c3b37c5c56fcd4d5a0f8df3 [file] [log] [blame]
Jon Miranda16ea1b12017-12-12 14:52:48 -08001/*
2 * Copyright (C) 2017 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 */
16package com.android.wallpaper.picker;
17
18import android.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.annotation.TargetApi;
21import android.app.Activity;
22import android.app.ProgressDialog;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.ActivityInfo;
26import android.content.res.Configuration;
27import android.content.res.Resources.NotFoundException;
28import android.graphics.Bitmap;
29import android.graphics.Bitmap.Config;
30import android.graphics.Color;
31import android.graphics.Point;
Santiago Etchebehere6af67ed2018-03-27 16:37:57 -070032import android.graphics.PointF;
Jon Miranda16ea1b12017-12-12 14:52:48 -080033import android.graphics.PorterDuff.Mode;
34import android.graphics.Rect;
Jon Miranda16ea1b12017-12-12 14:52:48 -080035import android.graphics.drawable.Drawable;
36import android.net.Uri;
Jon Miranda623e57e2018-01-05 16:33:30 -080037import android.os.Build;
Jon Miranda16ea1b12017-12-12 14:52:48 -080038import android.os.Build.VERSION;
39import android.os.Build.VERSION_CODES;
40import android.os.Bundle;
41import android.os.Handler;
Jon Miranda16ea1b12017-12-12 14:52:48 -080042import android.util.Log;
43import android.view.Display;
44import android.view.LayoutInflater;
45import android.view.Menu;
46import android.view.MenuInflater;
47import android.view.MenuItem;
48import android.view.Surface;
49import android.view.View;
50import android.view.View.OnClickListener;
51import android.view.ViewGroup;
52import android.view.Window;
53import android.widget.Button;
54import android.widget.FrameLayout;
55import android.widget.ImageView;
56import android.widget.LinearLayout;
57import android.widget.TextView;
58import android.widget.Toast;
59
60import com.android.wallpaper.R;
61import com.android.wallpaper.asset.Asset;
62import com.android.wallpaper.asset.Asset.BitmapReceiver;
63import com.android.wallpaper.asset.Asset.DimensionsReceiver;
Jon Miranda16ea1b12017-12-12 14:52:48 -080064import com.android.wallpaper.compat.BuildCompat;
65import com.android.wallpaper.compat.ButtonDrawableSetterCompat;
66import com.android.wallpaper.config.Flags;
Santiago Etchebehere8f118fe2018-06-11 16:57:10 -070067import com.android.wallpaper.model.LiveWallpaperInfo;
Jon Miranda16ea1b12017-12-12 14:52:48 -080068import com.android.wallpaper.model.WallpaperInfo;
Santiago Etchebehere8f118fe2018-06-11 16:57:10 -070069import com.android.wallpaper.module.CurrentWallpaperInfoFactory;
Jon Miranda16ea1b12017-12-12 14:52:48 -080070import com.android.wallpaper.module.ExploreIntentChecker;
71import com.android.wallpaper.module.Injector;
72import com.android.wallpaper.module.InjectorProvider;
73import com.android.wallpaper.module.UserEventLogger;
74import com.android.wallpaper.module.UserEventLogger.WallpaperSetFailureReason;
75import com.android.wallpaper.module.WallpaperPersister;
76import com.android.wallpaper.module.WallpaperPersister.Destination;
77import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
78import com.android.wallpaper.module.WallpaperPreferences;
79import com.android.wallpaper.util.ScreenSizeCalculator;
80import com.android.wallpaper.util.ThrowableAnalyzer;
81import com.android.wallpaper.util.WallpaperCropUtils;
82import com.android.wallpaper.widget.MaterialProgressDrawable;
Sunny Goyal8600a3f2018-08-15 12:48:01 -070083
Jon Miranda16ea1b12017-12-12 14:52:48 -080084import com.bumptech.glide.Glide;
85import com.bumptech.glide.MemoryCategory;
Jon Miranda623e57e2018-01-05 16:33:30 -080086import com.davemorrissey.labs.subscaleview.ImageSource;
87import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
Sunny Goyal8600a3f2018-08-15 12:48:01 -070088import com.google.android.material.bottomsheet.BottomSheetBehavior;
89import com.google.android.material.bottomsheet.BottomSheetBehavior.State;
Jon Miranda16ea1b12017-12-12 14:52:48 -080090
91import java.util.Date;
92import java.util.List;
93
Sunny Goyal8600a3f2018-08-15 12:48:01 -070094import androidx.annotation.IntDef;
95import androidx.appcompat.app.AppCompatActivity;
96import androidx.appcompat.widget.Toolbar;
97import androidx.core.view.ViewCompat;
98import androidx.fragment.app.Fragment;
99import androidx.fragment.app.FragmentActivity;
100
Jon Miranda16ea1b12017-12-12 14:52:48 -0800101/**
102 * Fragment which displays the UI for previewing an individual wallpaper and its attribution
103 * information.
104 */
105public class PreviewFragment extends Fragment implements
106 SetWallpaperDialogFragment.Listener, SetWallpaperErrorDialogFragment.Listener,
107 LoadWallpaperErrorDialogFragment.Listener {
108
109 /**
110 * User can view wallpaper and attributions in full screen, but "Set wallpaper" button is hidden.
111 */
112 public static final int MODE_VIEW_ONLY = 0;
113
114 /**
115 * User can view wallpaper and attributions in full screen and click "Set wallpaper" to set the
116 * wallpaper with pan and crop position to the device.
117 */
118 public static final int MODE_CROP_AND_SET_WALLPAPER = 1;
Jon Miranda623e57e2018-01-05 16:33:30 -0800119
120 /**
121 * Possible preview modes for the fragment.
122 */
123 @IntDef({
124 MODE_VIEW_ONLY,
125 MODE_CROP_AND_SET_WALLPAPER})
126 public @interface PreviewMode {
127 }
128
Clément Julliardea1638d2018-05-21 19:15:17 -0700129 protected static final String ARG_WALLPAPER = "wallpaper";
130 protected static final String ARG_PREVIEW_MODE = "preview_mode";
131 protected static final String ARG_TESTING_MODE_ENABLED = "testing_mode_enabled";
Jon Miranda16ea1b12017-12-12 14:52:48 -0800132 private static final String TAG_LOAD_WALLPAPER_ERROR_DIALOG_FRAGMENT =
133 "load_wallpaper_error_dialog";
134 private static final String TAG_SET_WALLPAPER_DIALOG_FRAGMENT = "set_wallpaper_dialog";
135 private static final String TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT =
136 "set_wallpaper_error_dialog";
137 private static final int UNUSED_REQUEST_CODE = 1;
138 private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f;
139 private static final String TAG = "PreviewFragment";
140 private static final String PROGRESS_DIALOG_NO_TITLE = null;
141 private static final boolean PROGRESS_DIALOG_INDETERMINATE = true;
142 private static final float PAGE_BITMAP_MAX_HEAP_RATIO = 0.25f;
143 private static final String KEY_BOTTOM_SHEET_STATE = "key_bottom_sheet_state";
Jon Miranda623e57e2018-01-05 16:33:30 -0800144
Jon Miranda16ea1b12017-12-12 14:52:48 -0800145 @PreviewMode
146 private int mPreviewMode;
Jon Miranda623e57e2018-01-05 16:33:30 -0800147
Jon Miranda16ea1b12017-12-12 14:52:48 -0800148 /**
149 * When true, enables a test mode of operation -- in which certain UI features are disabled to
150 * allow for UI tests to run correctly. Works around issue in ProgressDialog currently where the
151 * dialog constantly keeps the UI thread alive and blocks a test forever.
152 */
153 private boolean mTestingModeEnabled;
Jon Miranda623e57e2018-01-05 16:33:30 -0800154
Clément Julliardea1638d2018-05-21 19:15:17 -0700155 protected SubsamplingScaleImageView mFullResImageView;
156 protected WallpaperInfo mWallpaper;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800157 private Asset mWallpaperAsset;
158 private WallpaperPersister mWallpaperPersister;
159 private WallpaperPreferences mPreferences;
160 private UserEventLogger mUserEventLogger;
161 private LinearLayout mBottomSheet;
162 private TextView mAttributionTitle;
163 private TextView mAttributionSubtitle1;
164 private TextView mAttributionSubtitle2;
165 private FrameLayout mAttributionExploreSection;
166 private Button mAttributionExploreButton;
167 private ImageView mPreviewPaneArrow;
168 private int mCurrentScreenOrientation;
169 private ProgressDialog mProgressDialog;
170 private Point mDefaultCropSurfaceSize;
171 private Point mScreenSize;
172 private Point mRawWallpaperSize; // Native size of wallpaper image.
173 private ImageView mLoadingIndicator;
174 private MaterialProgressDrawable mProgressDrawable;
175 private ImageView mLowResImageView;
Jon Miranda623e57e2018-01-05 16:33:30 -0800176
Jon Miranda16ea1b12017-12-12 14:52:48 -0800177 @SuppressWarnings("RestrictTo")
178 @State
179 private int mBottomSheetInitialState;
Jon Miranda623e57e2018-01-05 16:33:30 -0800180
Jon Miranda16ea1b12017-12-12 14:52:48 -0800181 private Intent mExploreIntent;
Jon Miranda623e57e2018-01-05 16:33:30 -0800182
Jon Miranda16ea1b12017-12-12 14:52:48 -0800183 /**
184 * Staged error dialog fragments that were unable to be shown when the hosting activity didn't
185 * allow committing fragment transactions.
186 */
187 private SetWallpaperErrorDialogFragment mStagedSetWallpaperErrorDialogFragment;
188 private LoadWallpaperErrorDialogFragment mStagedLoadWallpaperErrorDialogFragment;
189
190 /**
191 * Creates and returns new instance of {@link PreviewFragment} with the provided wallpaper set as
192 * an argument.
193 */
194 public static PreviewFragment newInstance(
195 WallpaperInfo wallpaperInfo, @PreviewMode int mode, boolean testingModeEnabled) {
196 Bundle args = new Bundle();
197 args.putParcelable(ARG_WALLPAPER, wallpaperInfo);
198 args.putInt(ARG_PREVIEW_MODE, mode);
199 args.putBoolean(ARG_TESTING_MODE_ENABLED, testingModeEnabled);
200
201 PreviewFragment fragment = new PreviewFragment();
202 fragment.setArguments(args);
203 return fragment;
204 }
205
Jon Miranda16ea1b12017-12-12 14:52:48 -0800206 @Override
207 public void onCreate(Bundle savedInstanceState) {
208 super.onCreate(savedInstanceState);
209
210 Activity activity = getActivity();
211 Context appContext = activity.getApplicationContext();
212 Injector injector = InjectorProvider.getInjector();
213
214 mWallpaperPersister = injector.getWallpaperPersister(appContext);
215 mPreferences = injector.getPreferences(appContext);
216 mUserEventLogger = injector.getUserEventLogger(appContext);
217 mWallpaper = getArguments().getParcelable(ARG_WALLPAPER);
218 mWallpaperAsset = mWallpaper.getAsset(appContext);
219 //noinspection ResourceType
220 mPreviewMode = getArguments().getInt(ARG_PREVIEW_MODE);
221 mTestingModeEnabled = getArguments().getBoolean(ARG_TESTING_MODE_ENABLED);
222
223 setHasOptionsMenu(true);
224
225 // Allow the layout to draw fullscreen even behind the status bar, so we can set as the status
226 // bar color a color that has a custom translucency in the theme.
227 Window window = activity.getWindow();
228 window.getDecorView().setSystemUiVisibility(
229 View.SYSTEM_UI_FLAG_LAYOUT_STABLE
230 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
231 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
232
233 List<String> attributions = mWallpaper.getAttributions(activity);
234 if (attributions.size() > 0 && attributions.get(0) != null) {
235 activity.setTitle(attributions.get(0));
236 }
237 }
238
239 @Override
240 public View onCreateView(LayoutInflater inflater, ViewGroup container,
241 Bundle savedInstanceState) {
242 View view = inflater.inflate(R.layout.fragment_preview, container, false);
243
244 // Set toolbar as the action bar.
245 Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar);
246 ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
247 ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
248 ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayShowTitleEnabled(false);
249
250 // Use updated fancy arrow icon for O+.
251 if (BuildCompat.isAtLeastO()) {
252 Drawable navigationIcon = getResources().getDrawable(
253 R.drawable.material_ic_arrow_back_black_24);
254
255 // This Drawable's state is shared across the app, so make a copy of it before applying a
256 // color tint as not to affect other clients elsewhere in the app.
257 navigationIcon = navigationIcon.getConstantState().newDrawable().mutate();
258 navigationIcon.setColorFilter(
259 getResources().getColor(R.color.material_white_100), Mode.SRC_IN);
260 navigationIcon.setAutoMirrored(true);
261 toolbar.setNavigationIcon(navigationIcon);
262 }
263
264 ViewCompat.setPaddingRelative(toolbar,
265 /* start */ getResources().getDimensionPixelSize(
266 R.dimen.preview_toolbar_up_button_start_padding),
267 /* top */ 0,
268 /* end */ getResources().getDimensionPixelSize(
269 R.dimen.preview_toolbar_set_wallpaper_button_end_padding),
270 /* bottom */ 0);
271
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700272 mFullResImageView = view.findViewById(R.id.full_res_image);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800273 mLoadingIndicator = (ImageView) view.findViewById(R.id.loading_indicator);
274
275 mBottomSheet = (LinearLayout) view.findViewById(R.id.bottom_sheet);
276 mAttributionTitle = (TextView) view.findViewById(R.id.preview_attribution_pane_title);
277 mAttributionSubtitle1 = (TextView) view.findViewById(R.id.preview_attribution_pane_subtitle1);
278 mAttributionSubtitle2 = (TextView) view.findViewById(R.id.preview_attribution_pane_subtitle2);
279 mAttributionExploreSection = (FrameLayout) view.findViewById(
280 R.id.preview_attribution_pane_explore_section);
281 mAttributionExploreButton = (Button) view.findViewById(
282 R.id.preview_attribution_pane_explore_button);
283 mPreviewPaneArrow = (ImageView) view.findViewById(R.id.preview_attribution_pane_arrow);
284 mLowResImageView = (ImageView) view.findViewById(R.id.low_res_image);
285
286 mPreviewPaneArrow.setColorFilter(
287 getResources().getColor(R.color.preview_pane_arrow_color), Mode.SRC_IN);
288
289 // Trim some memory from Glide to make room for the full-size image in this fragment.
290 Glide.get(getActivity()).setMemoryCategory(MemoryCategory.LOW);
291
292 mDefaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
293 getResources(), getActivity().getWindowManager().getDefaultDisplay());
294 mScreenSize = ScreenSizeCalculator.getInstance().getScreenSize(
295 getActivity().getWindowManager().getDefaultDisplay());
296
297 // Load a low-res placeholder image if there's a thumbnail available from the asset that can be
298 // shown to the user more quickly than the full-sized image.
299 if (mWallpaperAsset.hasLowResDataSource()) {
300 mWallpaperAsset.loadLowResDrawable(getActivity(), mLowResImageView, Color.BLACK,
301 new WallpaperPreviewBitmapTransformation(getActivity().getApplicationContext(), isRtl()));
302 }
303
304 mWallpaperAsset.decodeRawDimensions(getActivity(), new DimensionsReceiver() {
305 @Override
306 public void onDimensionsDecoded(Point dimensions) {
307 // Don't continue loading the wallpaper if the Fragment is detached.
308 Activity activity = getActivity();
309 if (activity == null) {
310 return;
311 }
312
313 // Return early and show a dialog if dimensions are null (signaling a decoding error).
314 if (dimensions == null) {
315 showLoadWallpaperErrorDialog();
316 return;
317 }
318
319 mRawWallpaperSize = dimensions;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800320 ExploreIntentChecker intentChecker =
321 InjectorProvider.getInjector().getExploreIntentChecker(activity);
322 String actionUrl = mWallpaper.getActionUrl(activity);
323 if (actionUrl != null && !actionUrl.isEmpty()) {
324 Uri exploreUri = Uri.parse(mWallpaper.getActionUrl(activity));
325
Jon Miranda623e57e2018-01-05 16:33:30 -0800326 intentChecker.fetchValidActionViewIntent(exploreUri, exploreIntent -> {
327 if (getActivity() == null) {
328 return;
329 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800330
Jon Miranda623e57e2018-01-05 16:33:30 -0800331 mExploreIntent = exploreIntent;
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700332 initFullResView();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800333 });
334 } else {
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700335 initFullResView();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800336 }
337 }
338 });
339
340 // Configure loading indicator with a MaterialProgressDrawable.
341 mProgressDrawable =
342 new MaterialProgressDrawable(getActivity().getApplicationContext(), mLoadingIndicator);
343 mProgressDrawable.setAlpha(255);
344 mProgressDrawable.setBackgroundColor(getResources().getColor(R.color.material_white_100));
345 mProgressDrawable.setColorSchemeColors(getResources().getColor(R.color.accent_color));
346 mProgressDrawable.updateSizes(MaterialProgressDrawable.LARGE);
347 mLoadingIndicator.setImageDrawable(mProgressDrawable);
348
349 // We don't want to show the spinner every time we load an image if it loads quickly; instead,
350 // only start showing the spinner if loading the image has taken longer than half of a second.
351 mLoadingIndicator.postDelayed(new Runnable() {
352 @Override
353 public void run() {
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700354 if (mFullResImageView != null && !mFullResImageView.hasImage()
355 && !mTestingModeEnabled) {
Jon Miranda16ea1b12017-12-12 14:52:48 -0800356 mLoadingIndicator.setVisibility(View.VISIBLE);
357 mLoadingIndicator.setAlpha(1f);
358 if (mProgressDrawable != null) {
359 mProgressDrawable.start();
360 }
361 }
362 }
363 }, 500);
364
365 mBottomSheetInitialState = (savedInstanceState == null)
366 ? BottomSheetBehavior.STATE_EXPANDED
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700367 : savedInstanceState.getInt(KEY_BOTTOM_SHEET_STATE,
368 BottomSheetBehavior.STATE_EXPANDED);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800369 setUpBottomSheetListeners();
370
371 return view;
372 }
373
374 @Override
375 public void onResume() {
376 super.onResume();
377
378 WallpaperPreferences preferences = InjectorProvider.getInjector().getPreferences(getActivity());
379 preferences.setLastAppActiveTimestamp(new Date().getTime());
380
381 // Show the staged 'load wallpaper' or 'set wallpaper' error dialog fragments if there is one
382 // that was unable to be shown earlier when this fragment's hosting activity didn't allow
383 // committing fragment transactions.
384 if (mStagedLoadWallpaperErrorDialogFragment != null) {
385 mStagedLoadWallpaperErrorDialogFragment.show(
386 getFragmentManager(), TAG_LOAD_WALLPAPER_ERROR_DIALOG_FRAGMENT);
387 mStagedLoadWallpaperErrorDialogFragment = null;
388 }
389 if (mStagedSetWallpaperErrorDialogFragment != null) {
390 mStagedSetWallpaperErrorDialogFragment.show(
391 getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT);
392 mStagedSetWallpaperErrorDialogFragment = null;
393 }
394 }
395
396 @Override
397 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
398 super.onCreateOptionsMenu(menu, inflater);
399 inflater.inflate(R.menu.preview_menu, menu);
400 }
401
402 @Override
403 public void onPrepareOptionsMenu(Menu menu) {
404 super.onPrepareOptionsMenu(menu);
405 MenuItem setWallpaperButton = menu.findItem(R.id.set_wallpaper);
406
407 if (mPreviewMode == MODE_CROP_AND_SET_WALLPAPER && isWallpaperLoaded()) {
408 setWallpaperButton.setVisible(true);
409 } else {
410 setWallpaperButton.setVisible(false);
411 }
412 }
413
414 @Override
415 public boolean onOptionsItemSelected(MenuItem item) {
416 int id = item.getItemId();
417 if (id == R.id.set_wallpaper) {
418 if (BuildCompat.isAtLeastN()) {
Santiago Etchebehere8f118fe2018-06-11 16:57:10 -0700419 requestDestination();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800420 } else {
421 setCurrentWallpaper(WallpaperPersister.DEST_HOME_SCREEN);
422 }
423 return true;
424 } else if (id == android.R.id.home) {
425 // The Preview screen has multiple entry points. It could be opened from either
426 // the IndividualPreviewActivity, the "My photos" selection (by way of
427 // TopLevelPickerActivity), or from a system "crop and set wallpaper" intent.
428 // Therefore, handle the Up button as a global Back.
429 getActivity().onBackPressed();
430 return true;
431 }
432
433 return false;
434 }
435
Santiago Etchebehere8f118fe2018-06-11 16:57:10 -0700436 private void requestDestination() {
437 CurrentWallpaperInfoFactory factory = InjectorProvider.getInjector()
438 .getCurrentWallpaperFactory(getContext());
439
440 factory.createCurrentWallpaperInfos((homeWallpaper, lockWallpaper, presentationMode) -> {
441 SetWallpaperDialogFragment setWallpaperDialog = new SetWallpaperDialogFragment();
442 setWallpaperDialog.setTargetFragment(this, UNUSED_REQUEST_CODE);
443 if (homeWallpaper instanceof LiveWallpaperInfo && lockWallpaper == null) {
444 // if the lock wallpaper is a live wallpaper, we cannot set a home-only static one
445 setWallpaperDialog.setHomeOptionAvailable(false);
446 }
447 setWallpaperDialog.show(getFragmentManager(), TAG_SET_WALLPAPER_DIALOG_FRAGMENT);
448 }, true); // Force refresh as the wallpaper may have been set while this fragment was paused
449 }
450
Jon Miranda16ea1b12017-12-12 14:52:48 -0800451 @Override
452 public void onSetHomeScreen() {
453 setCurrentWallpaper(WallpaperPersister.DEST_HOME_SCREEN);
454 }
455
456 @Override
457 public void onSetLockScreen() {
458 setCurrentWallpaper(WallpaperPersister.DEST_LOCK_SCREEN);
459 }
460
461 @Override
462 public void onSetBoth() {
463 setCurrentWallpaper(WallpaperPersister.DEST_BOTH);
464 }
465
466 @Override
467 public void onClickTryAgain(@Destination int wallpaperDestination) {
468 setCurrentWallpaper(wallpaperDestination);
469 }
470
471 @Override
472 public void onClickOk() {
Santiago Etchebeheref790d702018-06-07 13:04:13 -0700473 FragmentActivity activity = getActivity();
474 if (activity != null) {
475 activity.finish();
476 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800477 }
478
479 @Override
480 public void onDestroy() {
481 super.onDestroy();
482 if (mProgressDialog != null) {
483 mProgressDialog.dismiss();
484 }
485 if (mProgressDrawable != null) {
486 mProgressDrawable.stop();
487 }
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700488 mFullResImageView.recycle();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800489 }
490
491 @Override
492 public void onSaveInstanceState(Bundle outState) {
493 super.onSaveInstanceState(outState);
494
495 final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
496 outState.putInt(KEY_BOTTOM_SHEET_STATE, bottomSheetBehavior.getState());
497 }
498
499 private void updatePreviewPaneArrow(int bottomSheetState) {
500 if (bottomSheetState == BottomSheetBehavior.STATE_COLLAPSED) {
501 mPreviewPaneArrow.setImageResource(R.drawable.material_ic_keyboard_arrow_up_black_24);
502 mPreviewPaneArrow.setContentDescription(
503 getResources().getString(R.string.expand_attribution_panel));
504 } else if (bottomSheetState == BottomSheetBehavior.STATE_EXPANDED) {
505 mPreviewPaneArrow.setImageResource(R.drawable.material_ic_keyboard_arrow_down_black_24);
506 mPreviewPaneArrow.setContentDescription(
507 getResources().getString(R.string.collapse_attribution_panel));
508 }
509 mPreviewPaneArrow.setColorFilter(
510 getResources().getColor(R.color.preview_pane_arrow_color), Mode.SRC_IN);
511 }
512
Jon Miranda623e57e2018-01-05 16:33:30 -0800513 /**
514 * Returns a zoom level that is similar to the actual zoom, but that is exactly 0.5 ** n for some
515 * integer n. This is useful for downsampling a bitmap--we want to see the bitmap at full detail,
516 * or downsampled to 1 in every 2 pixels, or 1 in 4, and so on, depending on the zoom.
517 */
518 private static float getDownsampleZoom(float actualZoom) {
519 if (actualZoom > 1) {
520 // Very zoomed in, but we can't sample more than 1 pixel per pixel.
521 return 1.0f;
522 }
523 float lower = 1.0f / roundUpToPower2((int) Math.ceil(1 / actualZoom));
524 float upper = lower * 2;
525 return nearestValue(actualZoom, lower, upper);
526 }
527
528 /**
529 * Returns the integer rounded up to the next power of 2.
530 */
531 private static int roundUpToPower2(int value) {
532 return 1 << (32 - Integer.numberOfLeadingZeros(value - 1));
533 }
534
535 /**
536 * Returns the closer of two values a and b to the given value.
537 */
538 private static float nearestValue(float value, float a, float b) {
539 return Math.abs(a - value) < Math.abs(b - value) ? a : b;
540 }
541
Jon Miranda16ea1b12017-12-12 14:52:48 -0800542 private void setUpBottomSheetListeners() {
543 final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
544
545 OnClickListener onClickListener = new OnClickListener() {
546 @Override
547 public void onClick(View view) {
548 if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
549 bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
550 } else if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
551 bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
552 }
553 }
554 };
555 mAttributionTitle.setOnClickListener(onClickListener);
556 mPreviewPaneArrow.setOnClickListener(onClickListener);
557
558 bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
559 @Override
560 public void onStateChanged(View bottomSheet, int newState) {
561 // Don't respond to lingering state change events occurring after the fragment has already
562 // been detached from the activity. Else, IllegalStateException may occur when trying to
563 // fetch resources.
564 if (getActivity() == null) {
565 return;
566 }
567
568 updatePreviewPaneArrow(newState);
569 }
570
571 @Override
572 public void onSlide(View bottomSheet, float slideOffset) {
573 float alpha;
574 if (slideOffset >= 0) {
575 alpha = slideOffset;
576 } else {
577 alpha = 1f - slideOffset;
578 }
579 mAttributionTitle.setAlpha(alpha);
580 mAttributionSubtitle1.setAlpha(alpha);
581 mAttributionSubtitle2.setAlpha(alpha);
582 mAttributionExploreButton.setAlpha(alpha);
583 }
584 });
585 }
586
587 private boolean isWallpaperLoaded() {
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700588 return mFullResImageView.hasImage();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800589 }
590
591 private void populateAttributionPane() {
592 final Context context = getContext();
593
594 final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet);
595
596 List<String> attributions = mWallpaper.getAttributions(context);
597 if (attributions.size() > 0 && attributions.get(0) != null) {
598 mAttributionTitle.setText(attributions.get(0));
599 }
600
601 if (attributions.size() > 1 && attributions.get(1) != null) {
602 mAttributionSubtitle1.setVisibility(View.VISIBLE);
603 mAttributionSubtitle1.setText(attributions.get(1));
604 }
605
606 if (attributions.size() > 2 && attributions.get(2) != null) {
607 mAttributionSubtitle2.setVisibility(View.VISIBLE);
608 mAttributionSubtitle2.setText(attributions.get(2));
609 }
610
611 String actionUrl = mWallpaper.getActionUrl(context);
612 if (actionUrl != null && !actionUrl.isEmpty()) {
613 if (mExploreIntent != null) {
614 if (Flags.skipDailyWallpaperButtonEnabled) {
Santiago Etchebehered24506c2018-04-05 17:02:42 -0700615 Drawable exploreButtonDrawable = context.getDrawable(
Santiago Etchebeheree0810d02018-05-10 17:39:40 -0700616 mWallpaper.getActionIconRes(context));
Jon Miranda16ea1b12017-12-12 14:52:48 -0800617
618 // This Drawable's state is shared across the app, so make a copy of it before applying a
619 // color tint as not to affect other clients elsewhere in the app.
Santiago Etchebehered1bd5092018-04-18 16:03:30 -0700620 exploreButtonDrawable = exploreButtonDrawable.getConstantState()
621 .newDrawable().mutate();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800622 // Color the "compass" icon with the accent color.
623 exploreButtonDrawable.setColorFilter(
624 getResources().getColor(R.color.accent_color), Mode.SRC_IN);
625 ButtonDrawableSetterCompat.setDrawableToButtonStart(
626 mAttributionExploreButton, exploreButtonDrawable);
Santiago Etchebehered1bd5092018-04-18 16:03:30 -0700627 mAttributionExploreButton.setText(context.getString(
Santiago Etchebeheree0810d02018-05-10 17:39:40 -0700628 mWallpaper.getActionLabelRes(context)));
Jon Miranda16ea1b12017-12-12 14:52:48 -0800629 }
630
631 mAttributionExploreSection.setVisibility(View.VISIBLE);
632 mAttributionExploreButton.setOnClickListener(new OnClickListener() {
633 @Override
634 public void onClick(View view) {
Santiago Etchebeherece5613f2018-06-01 13:22:47 -0700635 mUserEventLogger.logActionClicked(mWallpaper.getCollectionId(context),
636 mWallpaper.getActionLabelRes(context));
Jon Miranda16ea1b12017-12-12 14:52:48 -0800637
638 startActivity(mExploreIntent);
639 }
640 });
641 }
642 }
643
644 mBottomSheet.setVisibility(View.VISIBLE);
645
646 // Initialize the state of the BottomSheet based on the current state because if the initial
647 // and current state are the same, the state change listener won't fire and set the correct
648 // arrow asset and text alpha.
649 if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
650 updatePreviewPaneArrow(BottomSheetBehavior.STATE_EXPANDED);
651 } else {
652 mAttributionTitle.setAlpha(0f);
653 mAttributionSubtitle1.setAlpha(0f);
654 mAttributionSubtitle2.setAlpha(0f);
655 }
656
657 // Let the state change listener take care of animating a state change to the initial state if
658 // there's a state change.
659 bottomSheetBehavior.setState(mBottomSheetInitialState);
660 }
661
662 /**
663 * Initializes MosaicView by initializing tiling, setting a fallback page bitmap, and initializing
664 * a zoom-scroll observer and click listener.
665 */
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700666 private void initFullResView() {
667 mFullResImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP);
Jon Miranda623e57e2018-01-05 16:33:30 -0800668
669 // Set a solid black "page bitmap" so MosaicView draws a black background while waiting
670 // for the image to load or a transparent one if a thumbnail already loaded.
671 Bitmap blackBitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888);
672 int color = (mLowResImageView.getDrawable() == null) ? Color.BLACK : Color.TRANSPARENT;
673 blackBitmap.setPixel(0, 0, color);
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700674 mFullResImageView.setImage(ImageSource.bitmap(blackBitmap));
Jon Miranda623e57e2018-01-05 16:33:30 -0800675
Jon Miranda16ea1b12017-12-12 14:52:48 -0800676 // Then set a fallback "page bitmap" to cover the whole MosaicView, which is an actual
677 // (lower res) version of the image to be displayed.
Jon Miranda623e57e2018-01-05 16:33:30 -0800678 Point targetPageBitmapSize = new Point(mRawWallpaperSize);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800679 mWallpaperAsset.decodeBitmap(targetPageBitmapSize.x, targetPageBitmapSize.y,
680 new BitmapReceiver() {
681 @Override
682 public void onBitmapDecoded(Bitmap pageBitmap) {
683 // Check that the activity is still around since the decoding task started.
684 if (getActivity() == null) {
685 return;
686 }
687
688 // Some of these may be null depending on if the Fragment is paused, stopped,
689 // or destroyed.
690 if (mLoadingIndicator != null) {
691 mLoadingIndicator.setVisibility(View.GONE);
692 }
693 // The page bitmap may be null if there was a decoding error, so show an error dialog.
694 if (pageBitmap == null) {
695 showLoadWallpaperErrorDialog();
696 return;
697 }
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700698 if (mFullResImageView != null) {
Jon Miranda623e57e2018-01-05 16:33:30 -0800699 // Set page bitmap.
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700700 mFullResImageView.setImage(ImageSource.bitmap(pageBitmap));
Jon Miranda16ea1b12017-12-12 14:52:48 -0800701
Santiago Etchebehere6af67ed2018-03-27 16:37:57 -0700702 setDefaultWallpaperZoomAndScroll();
Jon Miranda623e57e2018-01-05 16:33:30 -0800703 crossFadeInMosaicView();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800704
Jon Miranda623e57e2018-01-05 16:33:30 -0800705 // Record memory snapshot of app one second delayed to allow time for MosaicView tiles
706 // to be decoded and overlaid on top of the page bitmap.
707 new Handler().postDelayed(new Runnable() {
708 @Override
709 public void run() {
710 if (getActivity() == null) {
711 return;
712 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800713
Jon Miranda623e57e2018-01-05 16:33:30 -0800714 InjectorProvider.getInjector().getPerformanceMonitor()
715 .recordFullResPreviewLoadedMemorySnapshot();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800716 }
Jon Miranda623e57e2018-01-05 16:33:30 -0800717 }, 1000);
718 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800719 if (mProgressDrawable != null) {
720 mProgressDrawable.stop();
721 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800722 getActivity().invalidateOptionsMenu();
723
724 populateAttributionPane();
725 }
726 });
727 }
728
729 /**
730 * Makes the MosaicView visible with an alpha fade-in animation while fading out the loading
731 * indicator.
732 */
733 private void crossFadeInMosaicView() {
734 long shortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
735
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700736 mFullResImageView.setAlpha(0f);
737 mFullResImageView.animate()
Jon Miranda16ea1b12017-12-12 14:52:48 -0800738 .alpha(1f)
Jon Miranda623e57e2018-01-05 16:33:30 -0800739 .setDuration(shortAnimationDuration)
740 .setListener(new AnimatorListenerAdapter() {
741 @Override
742 public void onAnimationEnd(Animator animation) {
743 // Clear the thumbnail bitmap reference to save memory since it's no longer visible.
744 if (mLowResImageView != null) {
745 mLowResImageView.setImageBitmap(null);
746 }
747 }
748 });
Jon Miranda16ea1b12017-12-12 14:52:48 -0800749
750 mLoadingIndicator.animate()
751 .alpha(0f)
752 .setDuration(shortAnimationDuration)
753 .setListener(new AnimatorListenerAdapter() {
754 @Override
755 public void onAnimationEnd(Animator animation) {
756 if (mLoadingIndicator != null) {
757 mLoadingIndicator.setVisibility(View.GONE);
758 }
759 }
760 });
761 }
762
Santiago Etchebehere6af67ed2018-03-27 16:37:57 -0700763 /**
764 * Sets the default wallpaper zoom and scroll position based on a "crop surface"
765 * (with extra width to account for parallax) superimposed on the screen. Shows as much of the
766 * wallpaper as possible on the crop surface and align screen to crop surface such that the
767 * default preview matches what would be seen by the user in the left-most home screen.
768 *
769 * <p>This method is called once in the Fragment lifecycle after the wallpaper asset has loaded
770 * and rendered to the layout.
771 */
772 private void setDefaultWallpaperZoomAndScroll() {
773 // Determine minimum zoom to fit maximum visible area of wallpaper on crop surface.
774 float defaultWallpaperZoom =
775 WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mDefaultCropSurfaceSize);
776 float minWallpaperZoom =
777 WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mScreenSize);
778
779 Point screenToCropSurfacePosition = WallpaperCropUtils.calculateCenterPosition(
780 mDefaultCropSurfaceSize, mScreenSize, true /* alignStart */, isRtl());
781 Point zoomedWallpaperSize = new Point(
782 Math.round(mRawWallpaperSize.x * defaultWallpaperZoom),
783 Math.round(mRawWallpaperSize.y * defaultWallpaperZoom));
784 Point cropSurfaceToWallpaperPosition = WallpaperCropUtils.calculateCenterPosition(
785 zoomedWallpaperSize, mDefaultCropSurfaceSize, false /* alignStart */, isRtl());
786
787 // Set min wallpaper zoom and max zoom on MosaicView widget.
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700788 mFullResImageView.setMaxScale(Math.max(DEFAULT_WALLPAPER_MAX_ZOOM, defaultWallpaperZoom));
789 mFullResImageView.setMinScale(minWallpaperZoom);
Santiago Etchebehere6af67ed2018-03-27 16:37:57 -0700790
791 // Set center to composite positioning between scaled wallpaper and screen.
792 PointF centerPosition = new PointF(
793 mRawWallpaperSize.x / 2f,
794 mRawWallpaperSize.y / 2f);
795 centerPosition.offset( - (screenToCropSurfacePosition.x + cropSurfaceToWallpaperPosition.x),
796 - (screenToCropSurfacePosition.y + cropSurfaceToWallpaperPosition.y));
797
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700798 mFullResImageView.setScaleAndCenter(defaultWallpaperZoom, centerPosition);
Santiago Etchebehere6af67ed2018-03-27 16:37:57 -0700799 }
800
Clément Julliardea1638d2018-05-21 19:15:17 -0700801 protected Rect calculateCropRect() {
Jon Miranda16ea1b12017-12-12 14:52:48 -0800802 // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom).
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700803 float wallpaperZoom = mFullResImageView.getScale();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800804 int scaledWallpaperWidth = (int) (mRawWallpaperSize.x * wallpaperZoom);
805 int scaledWallpaperHeight = (int) (mRawWallpaperSize.y * wallpaperZoom);
Jon Miranda623e57e2018-01-05 16:33:30 -0800806 Rect rect = new Rect();
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700807 mFullResImageView.visibleFileRect(rect);
Jon Miranda623e57e2018-01-05 16:33:30 -0800808 int scrollX = (int) (rect.left * wallpaperZoom);
809 int scrollY = (int) (rect.top * wallpaperZoom);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800810
Jon Miranda623e57e2018-01-05 16:33:30 -0800811 rect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800812 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(
813 getActivity().getWindowManager().getDefaultDisplay());
Jon Miranda16ea1b12017-12-12 14:52:48 -0800814 // Crop rect should start off as the visible screen and then include extra width and height if
815 // available within wallpaper at the current zoom.
816 Rect cropRect = new Rect(scrollX, scrollY, scrollX + screenSize.x, scrollY + screenSize.y);
817
818 Point defaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize(
819 getResources(), getActivity().getWindowManager().getDefaultDisplay());
820 int extraWidth = defaultCropSurfaceSize.x - screenSize.x;
821 int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - screenSize.y) / 2f);
822
823 // Try to increase size of screenRect to include extra width depending on the layout direction.
824 if (isRtl()) {
Jon Miranda623e57e2018-01-05 16:33:30 -0800825 cropRect.left = Math.max(cropRect.left - extraWidth, rect.left);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800826 } else {
Jon Miranda623e57e2018-01-05 16:33:30 -0800827 cropRect.right = Math.min(cropRect.right + extraWidth, rect.right);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800828 }
829
830 // Try to increase the size of the cropRect to to include extra height.
831 int availableExtraHeightTop = cropRect.top - Math.max(
Jon Miranda623e57e2018-01-05 16:33:30 -0800832 rect.top,
Jon Miranda16ea1b12017-12-12 14:52:48 -0800833 cropRect.top - extraHeightTopAndBottom);
834 int availableExtraHeightBottom = Math.min(
Jon Miranda623e57e2018-01-05 16:33:30 -0800835 rect.bottom,
Jon Miranda16ea1b12017-12-12 14:52:48 -0800836 cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom;
837
838 int availableExtraHeightTopAndBottom =
839 Math.min(availableExtraHeightTop, availableExtraHeightBottom);
840 cropRect.top -= availableExtraHeightTopAndBottom;
841 cropRect.bottom += availableExtraHeightTopAndBottom;
842
843 return cropRect;
844 }
845
846 /**
847 * Sets current wallpaper to the device based on current zoom and scroll state.
848 *
849 * @param destination The wallpaper destination i.e. home vs. lockscreen vs. both.
850 */
851 private void setCurrentWallpaper(@Destination final int destination) {
852 mPreferences.setPendingWallpaperSetStatus(WallpaperPreferences.WALLPAPER_SET_PENDING);
853
854 // Save current screen rotation so we can temporarily disable rotation while setting the
855 // wallpaper and restore after setting the wallpaper finishes.
856 saveAndLockScreenOrientation();
857
Jon Miranda623e57e2018-01-05 16:33:30 -0800858 // Clear MosaicView tiles and Glide's cache and pools to reclaim memory for final cropped
859 // bitmap.
Jon Miranda16ea1b12017-12-12 14:52:48 -0800860 Glide.get(getActivity()).clearMemory();
861
862 // ProgressDialog endlessly updates the UI thread, keeping it from going idle which therefore
863 // causes Espresso to hang once the dialog is shown.
864 if (!mTestingModeEnabled) {
865 int themeResId;
866 if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
867 themeResId = R.style.ProgressDialogThemePreL;
868 } else {
869 themeResId = R.style.LightDialogTheme;
870 }
871 mProgressDialog = new ProgressDialog(getActivity(), themeResId);
872
873 mProgressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE);
874 mProgressDialog.setMessage(
875 getResources().getString(R.string.set_wallpaper_progress_message));
876 mProgressDialog.setIndeterminate(PROGRESS_DIALOG_INDETERMINATE);
877 mProgressDialog.show();
878 }
879
Santiago Etchebehere41257a82018-03-30 10:50:12 -0700880 float wallpaperScale = mFullResImageView.getScale();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800881 Rect cropRect = calculateCropRect();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800882 mWallpaperPersister.setIndividualWallpaper(mWallpaper, mWallpaperAsset, cropRect,
883 wallpaperScale, destination, new SetWallpaperCallback() {
884 @Override
885 public void onSuccess() {
886 Context context = getContext();
887 mUserEventLogger.logWallpaperSet(
888 mWallpaper.getCollectionId(context),
889 mWallpaper.getWallpaperId());
890 mPreferences.setPendingWallpaperSetStatus(
891 WallpaperPreferences.WALLPAPER_SET_NOT_PENDING);
892 mUserEventLogger.logWallpaperSetResult(
893 UserEventLogger.WALLPAPER_SET_RESULT_SUCCESS);
894
895 if (getActivity() == null) {
896 return;
897 }
898
899 if (mProgressDialog != null) {
900 mProgressDialog.dismiss();
901 }
902
903 restoreScreenOrientation();
904 finishActivityWithResultOk();
905 }
906
907 @Override
908 public void onError(Throwable throwable) {
909 mPreferences.setPendingWallpaperSetStatus(
910 WallpaperPreferences.WALLPAPER_SET_NOT_PENDING);
911 mUserEventLogger.logWallpaperSetResult(
912 UserEventLogger.WALLPAPER_SET_RESULT_FAILURE);
913 @WallpaperSetFailureReason int failureReason = ThrowableAnalyzer.isOOM(throwable)
914 ? UserEventLogger.WALLPAPER_SET_FAILURE_REASON_OOM
915 : UserEventLogger.WALLPAPER_SET_FAILURE_REASON_OTHER;
916 mUserEventLogger.logWallpaperSetFailureReason(failureReason);
917
918 if (getActivity() == null) {
919 return;
920 }
921
922 if (mProgressDialog != null) {
923 mProgressDialog.dismiss();
924 }
925 restoreScreenOrientation();
926 showSetWallpaperErrorDialog(destination);
927 }
928 });
929 }
930
931 private void finishActivityWithResultOk() {
932 try {
933 Toast.makeText(
934 getActivity(), R.string.wallpaper_set_successfully_message, Toast.LENGTH_SHORT).show();
935 } catch (NotFoundException e) {
936 Log.e(TAG, "Could not show toast " + e);
937 }
938 getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
939 getActivity().setResult(Activity.RESULT_OK);
940 getActivity().finish();
941 }
942
943 private void showSetWallpaperErrorDialog(@Destination int wallpaperDestination) {
944 SetWallpaperErrorDialogFragment newFragment = SetWallpaperErrorDialogFragment.newInstance(
945 R.string.set_wallpaper_error_message, wallpaperDestination);
946 newFragment.setTargetFragment(this, UNUSED_REQUEST_CODE);
947
948 // Show 'set wallpaper' error dialog now if it's safe to commit fragment transactions, otherwise
949 // stage it for later when the hosting activity is in a state to commit fragment transactions.
950 BasePreviewActivity activity = (BasePreviewActivity) getActivity();
951 if (activity.isSafeToCommitFragmentTransaction()) {
952 newFragment.show(getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT);
953 } else {
954 mStagedSetWallpaperErrorDialogFragment = newFragment;
955 }
956 }
957
958 /**
959 * Shows 'load wallpaper' error dialog now or stage it to be shown when the hosting activity is in
960 * a state that allows committing fragment transactions.
961 */
962 private void showLoadWallpaperErrorDialog() {
963 LoadWallpaperErrorDialogFragment dialogFragment =
964 LoadWallpaperErrorDialogFragment.newInstance();
965 dialogFragment.setTargetFragment(PreviewFragment.this, UNUSED_REQUEST_CODE);
966
967 // Show 'load wallpaper' error dialog now or stage it to be shown when the hosting
968 // activity is in a state that allows committing fragment transactions.
969 BasePreviewActivity activity = (BasePreviewActivity) getActivity();
970 if (activity != null && activity.isSafeToCommitFragmentTransaction()) {
971 dialogFragment.show(PreviewFragment.this.getFragmentManager(),
972 TAG_LOAD_WALLPAPER_ERROR_DIALOG_FRAGMENT);
973 } else {
974 mStagedLoadWallpaperErrorDialogFragment = dialogFragment;
975 }
976 }
977
Jon Miranda623e57e2018-01-05 16:33:30 -0800978 @IntDef({
979 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT,
980 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE,
981 ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT,
982 ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE})
983 private @interface ActivityInfoScreenOrientation {
984 }
985
Jon Miranda16ea1b12017-12-12 14:52:48 -0800986 /**
987 * Gets the appropriate ActivityInfo orientation for the current configuration orientation to
988 * enable locking screen rotation at API levels lower than 18.
989 */
990 @ActivityInfoScreenOrientation
991 private int getCompatActivityInfoOrientation() {
992 int configOrientation = getResources().getConfiguration().orientation;
993 final Display display = getActivity().getWindowManager().getDefaultDisplay();
994 int naturalOrientation = Configuration.ORIENTATION_LANDSCAPE;
995 switch (display.getRotation()) {
996 case Surface.ROTATION_0:
997 case Surface.ROTATION_180:
998 // We are currently in the same basic orientation as the natural orientation.
999 naturalOrientation = configOrientation;
1000 break;
1001 case Surface.ROTATION_90:
1002 case Surface.ROTATION_270:
1003 // We are currently in the other basic orientation to the natural orientation.
1004 naturalOrientation = (configOrientation == Configuration.ORIENTATION_LANDSCAPE)
1005 ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
1006 break;
1007 default:
1008 // continue below
1009 }
1010
1011 // Since the map starts at portrait, we need to offset if this device's natural orientation
1012 // is landscape.
1013 int indexOffset = 0;
1014 if (naturalOrientation == Configuration.ORIENTATION_LANDSCAPE) {
1015 indexOffset = 1;
1016 }
1017
1018 switch ((display.getRotation() + indexOffset) % 4) {
1019 case 0:
1020 return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
1021 case 1:
1022 return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
1023 case 2:
1024 return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
1025 case 3:
1026 return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
1027 default:
1028 Log.e(TAG, "Display rotation did not correspond to a valid ActivityInfo orientation with"
1029 + " display rotation: " + display.getRotation() + " and index offset: " + indexOffset
1030 + ".");
1031 return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
1032 }
1033 }
1034
Jon Miranda623e57e2018-01-05 16:33:30 -08001035 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
Jon Miranda16ea1b12017-12-12 14:52:48 -08001036 private void saveAndLockScreenOrientation() {
1037 mCurrentScreenOrientation = getActivity().getRequestedOrientation();
1038 if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
1039 getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
1040 } else {
1041 getActivity().setRequestedOrientation(getCompatActivityInfoOrientation());
1042 }
1043 }
1044
1045 private void restoreScreenOrientation() {
1046 getActivity().setRequestedOrientation(mCurrentScreenOrientation);
1047 }
1048
1049 /**
1050 * Returns whether layout direction is RTL (or false for LTR). Since native RTL layout support was
1051 * added in API 17, returns false for versions lower than 17.
1052 */
1053 private boolean isRtl() {
Jon Miranda623e57e2018-01-05 16:33:30 -08001054 return VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1
Jon Miranda16ea1b12017-12-12 14:52:48 -08001055 && getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
1056 }
Jon Miranda623e57e2018-01-05 16:33:30 -08001057}