blob: 37c24779083cbe10d489b043bd2010c9992357ec [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.individual;
17
18import android.app.Activity;
19import android.app.ProgressDialog;
20import android.content.Context;
21import android.content.res.Resources.NotFoundException;
22import android.graphics.Point;
23import android.graphics.PorterDuff.Mode;
24import android.os.Build.VERSION;
25import android.os.Build.VERSION_CODES;
26import android.os.Bundle;
27import android.os.Handler;
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -070028import android.service.wallpaper.WallpaperService;
Jon Miranda16ea1b12017-12-12 14:52:48 -080029import android.util.Log;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.FrameLayout;
34import android.widget.ImageView;
35import android.widget.TextView;
36import android.widget.Toast;
37
38import com.android.wallpaper.R;
39import com.android.wallpaper.asset.Asset;
40import com.android.wallpaper.asset.Asset.DrawableLoadedListener;
41import com.android.wallpaper.config.Flags;
42import com.android.wallpaper.model.WallpaperCategory;
43import com.android.wallpaper.model.WallpaperInfo;
44import com.android.wallpaper.model.WallpaperReceiver;
45import com.android.wallpaper.model.WallpaperRotationInitializer;
46import com.android.wallpaper.model.WallpaperRotationInitializer.Listener;
47import com.android.wallpaper.model.WallpaperRotationInitializer.NetworkPreference;
48import com.android.wallpaper.model.WallpaperRotationInitializer.RotationInitializationState;
49import com.android.wallpaper.model.WallpaperRotationInitializer.RotationStateListener;
50import com.android.wallpaper.module.FormFactorChecker;
51import com.android.wallpaper.module.FormFactorChecker.FormFactor;
52import com.android.wallpaper.module.Injector;
53import com.android.wallpaper.module.InjectorProvider;
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -070054import com.android.wallpaper.module.PackageStatusNotifier;
Jon Miranda16ea1b12017-12-12 14:52:48 -080055import com.android.wallpaper.module.RotatingWallpaperComponentChecker;
56import com.android.wallpaper.module.WallpaperChangedNotifier;
57import com.android.wallpaper.module.WallpaperPersister;
58import com.android.wallpaper.module.WallpaperPersister.Destination;
59import com.android.wallpaper.module.WallpaperPreferences;
60import com.android.wallpaper.picker.BaseActivity;
61import com.android.wallpaper.picker.CurrentWallpaperBottomSheetPresenter;
62import com.android.wallpaper.picker.RotationStarter;
63import com.android.wallpaper.picker.SetWallpaperErrorDialogFragment;
64import com.android.wallpaper.picker.StartRotationDialogFragment;
65import com.android.wallpaper.picker.StartRotationErrorDialogFragment;
66import com.android.wallpaper.picker.WallpapersUiContainer;
67import com.android.wallpaper.picker.individual.SetIndividualHolder.OnSetListener;
68import com.android.wallpaper.util.DiskBasedLogger;
69import com.android.wallpaper.util.TileSizeCalculator;
70import com.android.wallpaper.widget.GridMarginDecoration;
Sunny Goyal8600a3f2018-08-15 12:48:01 -070071
Jon Miranda16ea1b12017-12-12 14:52:48 -080072import com.bumptech.glide.Glide;
73import com.bumptech.glide.MemoryCategory;
74
75import java.util.ArrayList;
76import java.util.Date;
77import java.util.List;
78import java.util.Random;
79
Sunny Goyal8600a3f2018-08-15 12:48:01 -070080import androidx.fragment.app.DialogFragment;
81import androidx.fragment.app.Fragment;
82import androidx.recyclerview.widget.GridLayoutManager;
83import androidx.recyclerview.widget.RecyclerView;
84import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
85import androidx.recyclerview.widget.RecyclerView.ViewHolder;
86
Jon Miranda16ea1b12017-12-12 14:52:48 -080087/**
88 * Displays the Main UI for picking an individual wallpaper image.
89 */
90public class IndividualPickerFragment extends Fragment
91 implements RotationStarter, StartRotationErrorDialogFragment.Listener,
92 CurrentWallpaperBottomSheetPresenter.RefreshListener,
93 SetWallpaperErrorDialogFragment.Listener {
94 private static final String TAG = "IndividualPickerFrgmnt";
95 private static final String ARG_CATEGORY_COLLECTION_ID = "category_collection_id";
96 private static final int UNUSED_REQUEST_CODE = 1;
97 private static final String TAG_START_ROTATION_DIALOG = "start_rotation_dialog";
98 private static final String TAG_START_ROTATION_ERROR_DIALOG = "start_rotation_error_dialog";
99 private static final String PROGRESS_DIALOG_NO_TITLE = null;
100 private static final boolean PROGRESS_DIALOG_INDETERMINATE = true;
101 private static final String TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT =
102 "individual_set_wallpaper_error_dialog";
103
104 /**
105 * Position of a special tile that doesn't belong to an individual wallpaper of the category,
106 * such as "my photos" or "daily rotation".
107 */
108 private static final int SPECIAL_FIXED_TILE_ADAPTER_POSITION = 0;
109
110 private WallpaperPreferences mWallpaperPreferences;
111 private WallpaperChangedNotifier mWallpaperChangedNotifier;
112 private RotatingWallpaperComponentChecker mRotatingWallpaperComponentChecker;
113 private RecyclerView mImageGrid;
114 private IndividualAdapter mAdapter;
115 private WallpaperCategory mCategory;
116 private WallpaperRotationInitializer mWallpaperRotationInitializer;
117 private List<WallpaperInfo> mWallpapers;
118 private Point mTileSizePx;
119 private ProgressDialog mProgressDialog;
120 private boolean mTestingMode;
121 private CurrentWallpaperBottomSheetPresenter mCurrentWallpaperBottomSheetPresenter;
122 private WallpapersUiContainer mWallpapersUiContainer;
123 @FormFactor
124 private int mFormFactor;
125 private SetIndividualHolder mPendingSetIndividualHolder;
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700126 private PackageStatusNotifier mPackageStatusNotifier;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800127
128 /**
129 * Staged error dialog fragments that were unable to be shown when the activity didn't allow
130 * committing fragment transactions.
131 */
132 private SetWallpaperErrorDialogFragment mStagedSetWallpaperErrorDialogFragment;
133 private StartRotationErrorDialogFragment mStagedStartRotationErrorDialogFragment;
134
135 private Handler mHandler;
136 private Runnable mCurrentWallpaperBottomSheetExpandedRunnable;
137 private Random mRandom;
138
139 /**
140 * Whether {@code mUpdateDailyWallpaperThumbRunnable} has been run at least once in this
141 * invocation of the fragment.
142 */
143 private boolean mWasUpdateRunnableRun;
144
145 /**
146 * A Runnable which regularly updates the thumbnail for the "Daily wallpapers" tile in desktop
147 * mode.
148 */
149 private Runnable mUpdateDailyWallpaperThumbRunnable = new Runnable() {
150 @Override
151 public void run() {
152 ViewHolder viewHolder = mImageGrid.findViewHolderForAdapterPosition(
153 SPECIAL_FIXED_TILE_ADAPTER_POSITION);
154 if (viewHolder instanceof DesktopRotationHolder) {
155 updateDesktopDailyRotationThumbnail((DesktopRotationHolder) viewHolder);
156 } else { // viewHolder is null
157 // If the rotation tile is unavailable (because user has scrolled down, causing the
158 // ViewHolder to be recycled), schedule the update for some time later. Once user scrolls up
159 // again, the ViewHolder will be re-bound and its thumbnail will be updated.
160 mHandler.postDelayed(mUpdateDailyWallpaperThumbRunnable,
161 DesktopRotationHolder.CROSSFADE_DURATION_MILLIS
162 + DesktopRotationHolder.CROSSFADE_DURATION_PAUSE_MILLIS);
163 }
164 }
165 };
166
167 private WallpaperChangedNotifier.Listener mWallpaperChangedListener = new WallpaperChangedNotifier.Listener() {
168 @Override
169 public void onWallpaperChanged() {
170 if (mFormFactor != FormFactorChecker.FORM_FACTOR_DESKTOP) {
171 return;
172 }
173
174 ViewHolder selectedViewHolder = mImageGrid.findViewHolderForAdapterPosition(
175 mAdapter.mSelectedAdapterPosition);
176
177 // Null remote ID => My Photos wallpaper, so deselect whatever was previously selected.
178 if (mWallpaperPreferences.getHomeWallpaperRemoteId() == null) {
179 if (selectedViewHolder instanceof SelectableHolder) {
180 ((SelectableHolder) selectedViewHolder).setSelectionState(
181 SelectableHolder.SELECTION_STATE_DESELECTED);
182 }
183 } else {
184 mAdapter.updateSelectedTile(mAdapter.mPendingSelectedAdapterPosition);
185 }
186 }
187 };
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700188 private PackageStatusNotifier.Listener mAppStatusListener;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800189
190 public static IndividualPickerFragment newInstance(String collectionId) {
191 Bundle args = new Bundle();
192 args.putString(ARG_CATEGORY_COLLECTION_ID, collectionId);
193
194 IndividualPickerFragment fragment = new IndividualPickerFragment();
195 fragment.setArguments(args);
196 return fragment;
197 }
198
199 private static int getResIdForRotationState(@RotationInitializationState int rotationState) {
200 switch (rotationState) {
201 case WallpaperRotationInitializer.ROTATION_NOT_INITIALIZED:
202 return R.string.daily_refresh_tile_subtitle;
203 case WallpaperRotationInitializer.ROTATION_HOME_ONLY:
204 return R.string.home_screen_message;
205 case WallpaperRotationInitializer.ROTATION_HOME_AND_LOCK:
206 return R.string.home_and_lock_short_label;
207 default:
208 Log.e(TAG, "Unknown rotation intialization state: " + rotationState);
209 return R.string.home_screen_message;
210 }
211 }
212
213 private void updateDesktopDailyRotationThumbnail(DesktopRotationHolder holder) {
214 int wallpapersIndex = mRandom.nextInt(mWallpapers.size());
215 Asset newThumbnailAsset = mWallpapers.get(wallpapersIndex).getThumbAsset(
216 getActivity());
217 holder.updateThumbnail(newThumbnailAsset, new DrawableLoadedListener() {
218 @Override
219 public void onDrawableLoaded() {
220 if (getActivity() == null) {
221 return;
222 }
223
224 // Schedule the next update of the thumbnail.
225 int delayMillis = DesktopRotationHolder.CROSSFADE_DURATION_MILLIS
226 + DesktopRotationHolder.CROSSFADE_DURATION_PAUSE_MILLIS;
227 mHandler.postDelayed(mUpdateDailyWallpaperThumbRunnable, delayMillis);
228 }
229 });
230 }
231
232 @Override
233 public void onCreate(Bundle savedInstanceState) {
234 super.onCreate(savedInstanceState);
235
236 Injector injector = InjectorProvider.getInjector();
237 Context appContext = getContext().getApplicationContext();
238 mWallpaperPreferences = injector.getPreferences(appContext);
239
240 mWallpaperChangedNotifier = WallpaperChangedNotifier.getInstance();
241 mWallpaperChangedNotifier.registerListener(mWallpaperChangedListener);
242
243 mRotatingWallpaperComponentChecker = injector.getRotatingWallpaperComponentChecker();
244
245 mFormFactor = injector.getFormFactorChecker(appContext).getFormFactor();
246
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700247 mPackageStatusNotifier = injector.getPackageStatusNotifier(appContext);
248
Jon Miranda16ea1b12017-12-12 14:52:48 -0800249 mWallpapers = new ArrayList<>();
250 mRandom = new Random();
251 mHandler = new Handler();
252
253 String collectionId = getArguments().getString(ARG_CATEGORY_COLLECTION_ID);
254 mCategory = (WallpaperCategory) injector.getCategoryProvider(appContext).getCategory(
255 collectionId);
256 if (mCategory == null) {
257 DiskBasedLogger.e(TAG, "Failed to find the category.", appContext);
258
259 // The absence of this category in the CategoryProvider indicates a broken state, probably due
260 // to a relaunch into this activity/fragment following a crash immediately prior; see
261 // b//38030129. Hence, finish the activity and return.
262 getActivity().finish();
263 return;
264 }
265
266 mWallpaperRotationInitializer = mCategory.getWallpaperRotationInitializer();
267
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700268 fetchWallpapers(false);
269
270 if (mCategory.supportsThirdParty()) {
271 mAppStatusListener = (packageName, status) -> {
272 if (status != PackageStatusNotifier.PackageStatus.REMOVED ||
273 mCategory.containsThirdParty(packageName)) {
274 fetchWallpapers(true);
275 }
276 };
277 mPackageStatusNotifier.addListener(mAppStatusListener,
278 WallpaperService.SERVICE_INTERFACE);
279 }
280 }
281
282 private void fetchWallpapers(boolean forceReload) {
283 mWallpapers.clear();
Jon Miranda16ea1b12017-12-12 14:52:48 -0800284 mCategory.fetchWallpapers(getActivity().getApplicationContext(), new WallpaperReceiver() {
285 @Override
286 public void onWallpapersReceived(List<WallpaperInfo> wallpapers) {
287 for (WallpaperInfo wallpaper : wallpapers) {
288 mWallpapers.add(wallpaper);
289 }
290
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700291 // Wallpapers may load after the adapter is initialized, in which case we have
292 // to explicitly notify that the data set has changed.
Jon Miranda16ea1b12017-12-12 14:52:48 -0800293 if (mAdapter != null) {
294 mAdapter.notifyDataSetChanged();
295 }
296
297 if (mWallpapersUiContainer != null) {
298 mWallpapersUiContainer.onWallpapersReady();
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700299 } else {
300 if (wallpapers.isEmpty()) {
301 // If there are no more wallpapers and we're on phone, just finish the
302 // Activity.
303 Activity activity = getActivity();
304 if (activity != null
305 && mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE) {
306 activity.finish();
307 }
308 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800309 }
310 }
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700311 }, forceReload);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800312 }
313
314 @Override
315 public View onCreateView(LayoutInflater inflater, ViewGroup container,
316 Bundle savedInstanceState) {
317 View view = inflater.inflate(R.layout.fragment_individual_picker, container, false);
318
319 mTileSizePx = TileSizeCalculator.getIndividualTileSize(getActivity());
320
321 mImageGrid = (RecyclerView) view.findViewById(R.id.wallpaper_grid);
322 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
323 int gridPaddingPx = getResources().getDimensionPixelSize(R.dimen.grid_padding_desktop);
324 updateImageGridPadding(false /* addExtraBottomSpace */);
325 mImageGrid.setScrollBarSize(gridPaddingPx);
326 }
327 GridMarginDecoration.applyTo(mImageGrid);
328
329 mAdapter = new IndividualAdapter(mWallpapers);
330 mImageGrid.setAdapter(mAdapter);
331 mImageGrid.setLayoutManager(new GridLayoutManager(getActivity(), getNumColumns()));
332
333 setUpBottomSheet();
334
335 return view;
336 }
337
338 @Override
339 public void onClickTryAgain(@Destination int unused) {
340 if (mPendingSetIndividualHolder != null) {
341 mPendingSetIndividualHolder.setWallpaper();
342 }
343 }
344
345 private void updateImageGridPadding(boolean addExtraBottomSpace) {
346 int gridPaddingPx = getResources().getDimensionPixelSize(R.dimen.grid_padding_desktop);
347 int bottomSheetHeightPx = getResources().getDimensionPixelSize(
348 R.dimen.current_wallpaper_bottom_sheet_layout_height);
349 int paddingBottomPx = addExtraBottomSpace ? bottomSheetHeightPx : 0;
350 // Only left and top may be set in order for the GridMarginDecoration to work properly.
351 mImageGrid.setPadding(
352 gridPaddingPx, gridPaddingPx, 0, paddingBottomPx);
353 }
354
355 /**
356 * Enables and populates the "Currently set" wallpaper BottomSheet.
357 */
358 private void setUpBottomSheet() {
359 mImageGrid.addOnScrollListener(new OnScrollListener() {
360 @Override
361 public void onScrolled(RecyclerView recyclerView, int dx, final int dy) {
362 if (mCurrentWallpaperBottomSheetPresenter == null) {
363 return;
364 }
365
366 if (mCurrentWallpaperBottomSheetExpandedRunnable != null) {
367 mHandler.removeCallbacks(mCurrentWallpaperBottomSheetExpandedRunnable);
368 }
369 mCurrentWallpaperBottomSheetExpandedRunnable = new Runnable() {
370 @Override
371 public void run() {
372 if (dy > 0) {
373 mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(false);
374 } else {
375 mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(true);
376 }
377 }
378 };
379 mHandler.postDelayed(mCurrentWallpaperBottomSheetExpandedRunnable, 100);
380 }
381 });
382 }
383
384 @Override
385 public void onResume() {
386 super.onResume();
387
388 WallpaperPreferences preferences = InjectorProvider.getInjector().getPreferences(getActivity());
389 preferences.setLastAppActiveTimestamp(new Date().getTime());
390
391 // Reset Glide memory settings to a "normal" level of usage since it may have been lowered in
392 // PreviewFragment.
393 Glide.get(getActivity()).setMemoryCategory(MemoryCategory.NORMAL);
394
395 // Show the staged 'start rotation' error dialog fragment if there is one that was unable to be
396 // shown earlier when this fragment's hosting activity didn't allow committing fragment
397 // transactions.
398 if (mStagedStartRotationErrorDialogFragment != null) {
399 mStagedStartRotationErrorDialogFragment.show(
400 getFragmentManager(), TAG_START_ROTATION_ERROR_DIALOG);
401 mStagedStartRotationErrorDialogFragment = null;
402 }
403
404 // Show the staged 'load wallpaper' or 'set wallpaper' error dialog fragments if there is one
405 // that was unable to be shown earlier when this fragment's hosting activity didn't allow
406 // committing fragment transactions.
407 if (mStagedSetWallpaperErrorDialogFragment != null) {
408 mStagedSetWallpaperErrorDialogFragment.show(
409 getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT);
410 mStagedSetWallpaperErrorDialogFragment = null;
411 }
412
413 if (isRotationEnabled()) {
414 if (mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE) {
415 // Refresh the state of the "start rotation" in case something changed the current daily
416 // rotation while this fragment was paused.
417 RotationHolder rotationHolder = (RotationHolder) mImageGrid
418 .findViewHolderForAdapterPosition(
419 SPECIAL_FIXED_TILE_ADAPTER_POSITION);
420 // The RotationHolder may be null if the RecyclerView has not created the view
421 // holder yet.
422 if (rotationHolder != null && Flags.dynamicStartRotationTileEnabled) {
423 refreshRotationHolder(rotationHolder);
424 }
425 } else if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
426 if (mWasUpdateRunnableRun && !mWallpapers.isEmpty()) {
427 // Must be resuming from a previously stopped state, so re-schedule the update of the
428 // daily wallpapers tile thumbnail.
429 mUpdateDailyWallpaperThumbRunnable.run();
430 }
431 }
432 }
433
434 }
435
436 @Override
437 public void onStop() {
438 super.onStop();
439 mHandler.removeCallbacks(mUpdateDailyWallpaperThumbRunnable);
440 }
441
442 @Override
443 public void onDestroy() {
444 super.onDestroy();
445 if (mProgressDialog != null) {
446 mProgressDialog.dismiss();
447 }
448 mWallpaperChangedNotifier.unregisterListener(mWallpaperChangedListener);
Santiago Etchebehere1ee76a22018-05-15 15:02:24 -0700449 if (mAppStatusListener != null) {
450 mPackageStatusNotifier.removeListener(mAppStatusListener);
451 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800452 }
453
454 @Override
455 public void retryStartRotation(@NetworkPreference int networkPreference) {
456 startRotation(networkPreference);
457 }
458
459 public void setCurrentWallpaperBottomSheetPresenter(
460 CurrentWallpaperBottomSheetPresenter presenter) {
461 mCurrentWallpaperBottomSheetPresenter = presenter;
462 }
463
464 public void setWallpapersUiContainer(WallpapersUiContainer uiContainer) {
465 mWallpapersUiContainer = uiContainer;
466 }
467
468 /**
469 * Enable a test mode of operation -- in which certain UI features are disabled to allow for
470 * UI tests to run correctly. Works around issue in ProgressDialog currently where the dialog
471 * constantly keeps the UI thread alive and blocks a test forever.
472 *
473 * @param testingMode
474 */
475 void setTestingMode(boolean testingMode) {
476 mTestingMode = testingMode;
477 }
478
479 /**
480 * Asynchronously fetches the refreshed rotation initialization state that is up to date with the
481 * state of the user's device and binds the state of the current category's rotation to the "start
482 * rotation" tile.
483 */
484 private void refreshRotationHolder(final RotationHolder rotationHolder) {
485 mWallpaperRotationInitializer.fetchRotationInitializationState(getContext(),
486 new RotationStateListener() {
487 @Override
488 public void onRotationStateReceived(
489 @RotationInitializationState final int rotationInitializationState) {
490
491 // Update the UI state of the "start rotation" tile displayed on screen. Do this in a
492 // Handler so it is scheduled at the end of the message queue. This is necessary to
493 // ensure we do not remove or add data from the adapter while the layout is still being
494 // computed. RecyclerView documentation therefore recommends performing such changes in
495 // a Handler.
496 new android.os.Handler().post(new Runnable() {
497 @Override
498 public void run() {
499 // A config change may have destroyed the activity since the refresh started, so
500 // check for that to avoid an NPE.
501 if (getActivity() == null) {
502 return;
503 }
504
505 rotationHolder.bindRotationInitializationState(rotationInitializationState);
506 }
507 });
508 }
509 });
510 }
511
512 @Override
513 public void startRotation(@NetworkPreference final int networkPreference) {
514 if (!isRotationEnabled()) {
515 Log.e(TAG, "Rotation is not enabled for this category " + mCategory.getTitle());
516 return;
517 }
518
519 // ProgressDialog endlessly updates the UI thread, keeping it from going idle which therefore
520 // causes Espresso to hang once the dialog is shown.
521 if (mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE && !mTestingMode) {
522 int themeResId;
523 if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
524 themeResId = R.style.ProgressDialogThemePreL;
525 } else {
526 themeResId = R.style.LightDialogTheme;
527 }
528 mProgressDialog = new ProgressDialog(getActivity(), themeResId);
529
530 mProgressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE);
531 mProgressDialog.setMessage(
532 getResources().getString(R.string.start_rotation_progress_message));
533 mProgressDialog.setIndeterminate(PROGRESS_DIALOG_INDETERMINATE);
534 mProgressDialog.show();
535 }
536
537 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
538 mAdapter.mPendingSelectedAdapterPosition = SPECIAL_FIXED_TILE_ADAPTER_POSITION;
539 }
540
541 final Context appContext = getActivity().getApplicationContext();
542
543 mWallpaperRotationInitializer.setFirstWallpaperInRotation(
544 appContext,
545 networkPreference,
546 new Listener() {
547 @Override
548 public void onFirstWallpaperInRotationSet() {
549 if (mProgressDialog != null) {
550 mProgressDialog.dismiss();
551 }
552
553 // The fragment may be detached from its containing activity if the user exits the
554 // app before the first wallpaper image in rotation finishes downloading.
555 Activity activity = getActivity();
556
557 if (activity != null
558 && mWallpaperRotationInitializer
559 .isNoBackupImageWallpaperPreviewNeeded(appContext)) {
560 ((IndividualPickerActivity) activity).showNoBackupImageWallpaperPreview();
561 } else {
562 if (mWallpaperRotationInitializer.startRotation(appContext)) {
563 if (activity != null && mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE) {
564 try {
565 Toast.makeText(getActivity(), R.string.wallpaper_set_successfully_message,
566 Toast.LENGTH_SHORT).show();
567 } catch (NotFoundException e) {
568 Log.e(TAG, "Could not show toast " + e);
569 }
570
571 activity.setResult(Activity.RESULT_OK);
572 activity.finish();
573 } else if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
574 mAdapter.updateSelectedTile(SPECIAL_FIXED_TILE_ADAPTER_POSITION);
575 }
576 } else { // Failed to start rotation.
577 showStartRotationErrorDialog(networkPreference);
578
579 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
580 DesktopRotationHolder rotationViewHolder =
581 (DesktopRotationHolder) mImageGrid.findViewHolderForAdapterPosition(
582 SPECIAL_FIXED_TILE_ADAPTER_POSITION);
583 rotationViewHolder.setSelectionState(SelectableHolder.SELECTION_STATE_DESELECTED);
584 }
585 }
586 }
587 }
588
589 @Override
590 public void onError() {
591 if (mProgressDialog != null) {
592 mProgressDialog.dismiss();
593 }
594
595 showStartRotationErrorDialog(networkPreference);
596
597 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
598 DesktopRotationHolder rotationViewHolder =
599 (DesktopRotationHolder) mImageGrid.findViewHolderForAdapterPosition(
600 SPECIAL_FIXED_TILE_ADAPTER_POSITION);
601 rotationViewHolder.setSelectionState(SelectableHolder.SELECTION_STATE_DESELECTED);
602 }
603 }
604 });
605 }
606
607 private void showStartRotationErrorDialog(@NetworkPreference int networkPreference) {
608 BaseActivity activity = (BaseActivity) getActivity();
609 if (activity != null) {
610 StartRotationErrorDialogFragment startRotationErrorDialogFragment =
611 StartRotationErrorDialogFragment.newInstance(networkPreference);
612 startRotationErrorDialogFragment.setTargetFragment(
613 IndividualPickerFragment.this, UNUSED_REQUEST_CODE);
614
615 if (activity.isSafeToCommitFragmentTransaction()) {
616 startRotationErrorDialogFragment.show(
617 getFragmentManager(), TAG_START_ROTATION_ERROR_DIALOG);
618 } else {
619 mStagedStartRotationErrorDialogFragment = startRotationErrorDialogFragment;
620 }
621 }
622 }
623
624 private int getNumColumns() {
625 return TileSizeCalculator.getNumIndividualColumns(getActivity());
626 }
627
628 /**
629 * Returns whether rotation is enabled for this category.
630 */
631 private boolean isRotationEnabled() {
632 boolean isRotationSupported =
633 mRotatingWallpaperComponentChecker.getRotatingWallpaperSupport(getContext())
634 == RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_SUPPORT_SUPPORTED;
635
636 return isRotationSupported && mWallpaperRotationInitializer != null;
637 }
638
639 @Override
640 public void onCurrentWallpaperRefreshed() {
641 mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(true);
642 }
643
644 /**
645 * Shows a "set wallpaper" error dialog with a failure message and button to try again.
646 */
647 private void showSetWallpaperErrorDialog() {
648 SetWallpaperErrorDialogFragment dialogFragment = SetWallpaperErrorDialogFragment.newInstance(
649 R.string.set_wallpaper_error_message, WallpaperPersister.DEST_BOTH);
650 dialogFragment.setTargetFragment(this, UNUSED_REQUEST_CODE);
651
652 if (((BaseActivity) getActivity()).isSafeToCommitFragmentTransaction()) {
653 dialogFragment.show(getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT);
654 } else {
655 mStagedSetWallpaperErrorDialogFragment = dialogFragment;
656 }
657 }
658
659 /**
660 * ViewHolder subclass for "daily refresh" tile in the RecyclerView, only shown if rotation is
661 * enabled for this category.
662 */
663 private class RotationHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
664
665 private FrameLayout mTileLayout;
666 private TextView mRotationMessage;
667 private TextView mRotationTitle;
668 private ImageView mRefreshIcon;
669
670 RotationHolder(View itemView) {
671 super(itemView);
672 itemView.setOnClickListener(this);
673
674 mTileLayout = (FrameLayout) itemView.findViewById(R.id.daily_refresh);
675 mRotationMessage = (TextView) itemView.findViewById(R.id.rotation_tile_message);
676 mRotationTitle = (TextView) itemView.findViewById(R.id.rotation_tile_title);
677 mRefreshIcon = (ImageView) itemView.findViewById(R.id.rotation_tile_refresh_icon);
678 mTileLayout.getLayoutParams().height = mTileSizePx.y;
679
680 // If the feature flag for "dynamic start rotation tile" is not enabled, fall back to the
681 // static UI with a blue accent color background and "Tap to turn on" text.
682 if (!Flags.dynamicStartRotationTileEnabled) {
683 mTileLayout.setBackgroundColor(
684 getResources().getColor(R.color.rotation_tile_enabled_background_color));
685 mRotationMessage.setText(R.string.daily_refresh_tile_subtitle);
686 mRotationTitle.setTextColor(
687 getResources().getColor(R.color.rotation_tile_enabled_title_text_color));
688 mRotationMessage.setTextColor(
689 getResources().getColor(R.color.rotation_tile_enabled_subtitle_text_color));
690 mRefreshIcon.setColorFilter(
691 getResources().getColor(R.color.rotation_tile_enabled_refresh_icon_color), Mode.SRC_IN);
692 return;
693 }
694
695 // Initialize the state of the "start rotation" tile (i.e., whether it is gray or blue to
696 // indicate if rotation is turned on for the current category) with last-known rotation state
697 // that could be stale. The last-known rotation state is correct in most cases and is a good
698 // starting point but may not be accurate if the user set a wallpaper through a 3rd party app
699 // while this app was paused.
700 int rotationState = mWallpaperRotationInitializer.getRotationInitializationStateDirty(
701 getContext());
702 bindRotationInitializationState(rotationState);
703 }
704
705 @Override
706 public void onClick(View v) {
707 boolean isLiveWallpaperNeeded = mWallpaperRotationInitializer
708 .isNoBackupImageWallpaperPreviewNeeded(getActivity().getApplicationContext());
709 DialogFragment startRotationDialogFragment = StartRotationDialogFragment
710 .newInstance(isLiveWallpaperNeeded);
711 startRotationDialogFragment.setTargetFragment(
712 IndividualPickerFragment.this, UNUSED_REQUEST_CODE);
713 startRotationDialogFragment.show(getFragmentManager(), TAG_START_ROTATION_DIALOG);
714 }
715
716 /**
717 * Binds the provided rotation initialization state to the RotationHolder and updates the tile's
718 * UI to be in sync with the state (i.e., message and color appropriately reflect the state to
719 * the user).
720 */
721 void bindRotationInitializationState(@RotationInitializationState int rotationState) {
722 int newBackgroundColor =
723 (rotationState == WallpaperRotationInitializer.ROTATION_NOT_INITIALIZED)
724 ? getResources().getColor(R.color.rotation_tile_not_enabled_background_color)
725 : getResources().getColor(R.color.rotation_tile_enabled_background_color);
726 int newTitleTextColor =
727 (rotationState == WallpaperRotationInitializer.ROTATION_NOT_INITIALIZED)
728 ? getResources().getColor(R.color.rotation_tile_not_enabled_title_text_color)
729 : getResources().getColor(R.color.rotation_tile_enabled_title_text_color);
730 int newSubtitleTextColor =
731 (rotationState == WallpaperRotationInitializer.ROTATION_NOT_INITIALIZED)
732 ? getResources().getColor(R.color.rotation_tile_not_enabled_subtitle_text_color)
733 : getResources().getColor(R.color.rotation_tile_enabled_subtitle_text_color);
734 int newRefreshIconColor =
735 (rotationState == WallpaperRotationInitializer.ROTATION_NOT_INITIALIZED)
736 ? getResources().getColor(R.color.rotation_tile_not_enabled_refresh_icon_color)
737 : getResources().getColor(R.color.rotation_tile_enabled_refresh_icon_color);
738
739 mTileLayout.setBackgroundColor(newBackgroundColor);
740 mRotationTitle.setTextColor(newTitleTextColor);
741 mRotationMessage.setText(getResIdForRotationState(rotationState));
742 mRotationMessage.setTextColor(newSubtitleTextColor);
743 mRefreshIcon.setColorFilter(newRefreshIconColor, Mode.SRC_IN);
744 }
745 }
746
747 /**
748 * RecyclerView Adapter subclass for the wallpaper tiles in the RecyclerView.
749 */
750 private class IndividualAdapter extends RecyclerView.Adapter<ViewHolder> {
751 private static final int ITEM_VIEW_TYPE_ROTATION = 1;
752 private static final int ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER = 2;
753 private static final int ITEM_VIEW_TYPE_MY_PHOTOS = 3;
754
755 private final List<WallpaperInfo> mWallpapers;
756
757 private int mPendingSelectedAdapterPosition;
758 private int mSelectedAdapterPosition;
759
760 IndividualAdapter(List<WallpaperInfo> wallpapers) {
761 mWallpapers = wallpapers;
762 mPendingSelectedAdapterPosition = -1;
763 mSelectedAdapterPosition = -1;
764 }
765
766 @Override
767 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
768 switch (viewType) {
769 case ITEM_VIEW_TYPE_ROTATION:
770 return createRotationHolder(parent);
771 case ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER:
772 return createIndividualHolder(parent);
773 case ITEM_VIEW_TYPE_MY_PHOTOS:
774 return createMyPhotosHolder(parent);
775 default:
776 Log.e(TAG, "Unsupported viewType " + viewType + " in IndividualAdapter");
777 return null;
778 }
779 }
780
781 @Override
782 public int getItemViewType(int position) {
783 if (isRotationEnabled() && position == SPECIAL_FIXED_TILE_ADAPTER_POSITION) {
784 return ITEM_VIEW_TYPE_ROTATION;
785 }
786
787 // A category cannot have both a "start rotation" tile and a "my photos" tile.
788 if (mCategory.supportsCustomPhotos()
789 && !isRotationEnabled()
790 && position == SPECIAL_FIXED_TILE_ADAPTER_POSITION) {
791 return ITEM_VIEW_TYPE_MY_PHOTOS;
792 }
793
794 return ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER;
795 }
796
797 @Override
798 public void onBindViewHolder(ViewHolder holder, int position) {
799 int viewType = getItemViewType(position);
800
801 switch (viewType) {
802 case ITEM_VIEW_TYPE_ROTATION:
803 onBindRotationHolder(holder, position);
804 break;
805 case ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER:
806 onBindIndividualHolder(holder, position);
807 break;
808 case ITEM_VIEW_TYPE_MY_PHOTOS:
809 ((MyPhotosViewHolder) holder).bind();
810 break;
811 default:
812 Log.e(TAG, "Unsupported viewType " + viewType + " in IndividualAdapter");
813 }
814 }
815
816 @Override
817 public int getItemCount() {
818 return (isRotationEnabled() || mCategory.supportsCustomPhotos())
819 ? mWallpapers.size() + 1
820 : mWallpapers.size();
821 }
822
823 private ViewHolder createRotationHolder(ViewGroup parent) {
824 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
825 View view;
826
827 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
828 view = layoutInflater.inflate(R.layout.grid_item_rotation_desktop, parent, false);
829 SelectionAnimator selectionAnimator =
830 new CheckmarkSelectionAnimator(getActivity(), view);
831 return new DesktopRotationHolder(
832 getActivity(), mTileSizePx.y, view, selectionAnimator,
833 IndividualPickerFragment.this);
834 } else { // MOBILE
835 view = layoutInflater.inflate(R.layout.grid_item_rotation, parent, false);
836 return new RotationHolder(view);
837 }
838 }
839
840 private ViewHolder createIndividualHolder(ViewGroup parent) {
841 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
842 View view = layoutInflater.inflate(R.layout.grid_item_image, parent, false);
843
844 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
845 SelectionAnimator selectionAnimator =
846 new CheckmarkSelectionAnimator(getActivity(), view);
847 return new SetIndividualHolder(
848 getActivity(), mTileSizePx.y, view,
849 selectionAnimator,
850 new OnSetListener() {
851 @Override
852 public void onPendingWallpaperSet(int adapterPosition) {
853 // Deselect and hide loading indicator for any previously pending tile.
854 if (mPendingSelectedAdapterPosition != -1) {
855 ViewHolder oldViewHolder = mImageGrid.findViewHolderForAdapterPosition(
856 mPendingSelectedAdapterPosition);
857 if (oldViewHolder instanceof SelectableHolder) {
858 ((SelectableHolder) oldViewHolder).setSelectionState(
859 SelectableHolder.SELECTION_STATE_DESELECTED);
860 }
861 }
862
863 if (mSelectedAdapterPosition != -1) {
864 ViewHolder oldViewHolder = mImageGrid.findViewHolderForAdapterPosition(
865 mSelectedAdapterPosition);
866 if (oldViewHolder instanceof SelectableHolder) {
867 ((SelectableHolder) oldViewHolder).setSelectionState(
868 SelectableHolder.SELECTION_STATE_DESELECTED);
869 }
870 }
871
872 mPendingSelectedAdapterPosition = adapterPosition;
873 }
874
875 @Override
876 public void onWallpaperSet(int adapterPosition) {
877 // No-op -- UI handles a new wallpaper being set by reacting to the
878 // WallpaperChangedNotifier.
879 }
880
881 @Override
882 public void onWallpaperSetFailed(SetIndividualHolder holder) {
883 showSetWallpaperErrorDialog();
884 mPendingSetIndividualHolder = holder;
885 }
886 });
887 } else { // MOBILE
888 return new PreviewIndividualHolder(
889 (IndividualPickerActivity) getActivity(), mTileSizePx.y, view);
890 }
891 }
892
893 private ViewHolder createMyPhotosHolder(ViewGroup parent) {
894 LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
895 View view = layoutInflater.inflate(R.layout.grid_item_my_photos, parent, false);
896
897 return new MyPhotosViewHolder(getActivity(), mTileSizePx.y, view);
898 }
899
900 /**
901 * Marks the tile at the given position as selected with a visual indication. Also updates the
902 * "currently selected" BottomSheet to reflect the newly selected tile.
903 */
904 private void updateSelectedTile(int newlySelectedPosition) {
905 // Prevent multiple spinners from appearing with a user tapping several tiles in rapid
906 // succession.
907 if (mPendingSelectedAdapterPosition == mSelectedAdapterPosition) {
908 return;
909 }
910
911 if (mCurrentWallpaperBottomSheetPresenter != null) {
912 mCurrentWallpaperBottomSheetPresenter.refreshCurrentWallpapers(
913 IndividualPickerFragment.this);
914
915 if (mCurrentWallpaperBottomSheetExpandedRunnable != null) {
916 mHandler.removeCallbacks(mCurrentWallpaperBottomSheetExpandedRunnable);
917 }
918 mCurrentWallpaperBottomSheetExpandedRunnable = new Runnable() {
919 @Override
920 public void run() {
921 mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(true);
922 }
923 };
924 mHandler.postDelayed(mCurrentWallpaperBottomSheetExpandedRunnable, 100);
925 }
926
927 // User may have switched to another category, thus detaching this fragment, so check here.
928 // NOTE: We do this check after updating the current wallpaper BottomSheet so that the update
929 // still occurs in the UI after the user selects that other category.
930 if (getActivity() == null) {
931 return;
932 }
933
934 // Update the newly selected wallpaper ViewHolder and the old one so that if
935 // selection UI state applies (desktop UI), it is updated.
936 if (mSelectedAdapterPosition >= 0) {
937 ViewHolder oldViewHolder = mImageGrid.findViewHolderForAdapterPosition(
938 mSelectedAdapterPosition);
939 if (oldViewHolder instanceof SelectableHolder) {
940 ((SelectableHolder) oldViewHolder).setSelectionState(
941 SelectableHolder.SELECTION_STATE_DESELECTED);
942 }
943 }
944
945 // Animate selection of newly selected tile.
946 ViewHolder newViewHolder = mImageGrid
947 .findViewHolderForAdapterPosition(newlySelectedPosition);
948 if (newViewHolder instanceof SelectableHolder) {
949 ((SelectableHolder) newViewHolder).setSelectionState(
950 SelectableHolder.SELECTION_STATE_SELECTED);
951 }
952
953 mSelectedAdapterPosition = newlySelectedPosition;
954
955 // If the tile was in the last row of the grid, add space below it so the user can scroll down
956 // and up to see the BottomSheet without it fully overlapping the newly selected tile.
957 int spanCount = ((GridLayoutManager) mImageGrid.getLayoutManager()).getSpanCount();
958 int numRows = (int) Math.ceil((float) getItemCount() / spanCount);
959 int rowOfNewlySelectedTile = newlySelectedPosition / spanCount;
960 boolean isInLastRow = rowOfNewlySelectedTile == numRows - 1;
961
962 updateImageGridPadding(isInLastRow /* addExtraBottomSpace */);
963 }
964
965 private void onBindRotationHolder(ViewHolder holder, int position) {
966 if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
967 String collectionId = mCategory.getCollectionId();
968 ((DesktopRotationHolder) holder).bind(collectionId);
969
970 if (mWallpaperPreferences.getWallpaperPresentationMode()
971 == WallpaperPreferences.PRESENTATION_MODE_ROTATING
972 && collectionId.equals(mWallpaperPreferences.getHomeWallpaperCollectionId())) {
973 mSelectedAdapterPosition = position;
974 }
975
976 if (!mWasUpdateRunnableRun && !mWallpapers.isEmpty()) {
977 updateDesktopDailyRotationThumbnail((DesktopRotationHolder) holder);
978 mWasUpdateRunnableRun = true;
979 }
980 }
981 }
982
983 private void onBindIndividualHolder(ViewHolder holder, int position) {
984 int wallpaperIndex = (isRotationEnabled() || mCategory.supportsCustomPhotos())
985 ? position - 1 : position;
986 WallpaperInfo wallpaper = mWallpapers.get(wallpaperIndex);
987 ((IndividualHolder) holder).bindWallpaper(wallpaper);
988 WallpaperPreferences prefs = InjectorProvider.getInjector().getPreferences(getContext());
989
990 String wallpaperId = wallpaper.getWallpaperId();
991 if (wallpaperId != null && wallpaperId.equals(prefs.getHomeWallpaperRemoteId())) {
992 mSelectedAdapterPosition = position;
993 }
994 }
995 }
996}