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