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