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