1 /*
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  */
16 package com.android.wallpaper.picker.individual;
17 
18 import android.annotation.MenuRes;
19 import android.app.Activity;
20 import android.app.ProgressDialog;
21 import android.app.WallpaperManager;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.content.res.Configuration;
26 import android.content.res.Resources.NotFoundException;
27 import android.graphics.Point;
28 import android.graphics.Rect;
29 import android.os.Build.VERSION;
30 import android.os.Build.VERSION_CODES;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.service.wallpaper.WallpaperService;
34 import android.text.TextUtils;
35 import android.util.ArraySet;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.widget.ImageView;
42 import android.widget.RelativeLayout;
43 import android.widget.Toast;
44 
45 import androidx.annotation.DrawableRes;
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.cardview.widget.CardView;
49 import androidx.core.widget.ContentLoadingProgressBar;
50 import androidx.fragment.app.DialogFragment;
51 import androidx.fragment.app.Fragment;
52 import androidx.recyclerview.widget.GridLayoutManager;
53 import androidx.recyclerview.widget.RecyclerView;
54 import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
55 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
56 
57 import com.android.wallpaper.R;
58 import com.android.wallpaper.asset.Asset;
59 import com.android.wallpaper.asset.Asset.DrawableLoadedListener;
60 import com.android.wallpaper.model.Category;
61 import com.android.wallpaper.model.CategoryProvider;
62 import com.android.wallpaper.model.CategoryReceiver;
63 import com.android.wallpaper.model.LiveWallpaperInfo;
64 import com.android.wallpaper.model.WallpaperCategory;
65 import com.android.wallpaper.model.WallpaperInfo;
66 import com.android.wallpaper.model.WallpaperReceiver;
67 import com.android.wallpaper.model.WallpaperRotationInitializer;
68 import com.android.wallpaper.model.WallpaperRotationInitializer.Listener;
69 import com.android.wallpaper.model.WallpaperRotationInitializer.NetworkPreference;
70 import com.android.wallpaper.module.FormFactorChecker;
71 import com.android.wallpaper.module.FormFactorChecker.FormFactor;
72 import com.android.wallpaper.module.Injector;
73 import com.android.wallpaper.module.InjectorProvider;
74 import com.android.wallpaper.module.PackageStatusNotifier;
75 import com.android.wallpaper.module.UserEventLogger;
76 import com.android.wallpaper.module.WallpaperChangedNotifier;
77 import com.android.wallpaper.module.WallpaperPersister;
78 import com.android.wallpaper.module.WallpaperPersister.Destination;
79 import com.android.wallpaper.module.WallpaperPreferences;
80 import com.android.wallpaper.module.WallpaperSetter;
81 import com.android.wallpaper.picker.AppbarFragment;
82 import com.android.wallpaper.picker.BaseActivity;
83 import com.android.wallpaper.picker.CurrentWallpaperBottomSheetPresenter;
84 import com.android.wallpaper.picker.FragmentTransactionChecker;
85 import com.android.wallpaper.picker.MyPhotosStarter.MyPhotosStarterProvider;
86 import com.android.wallpaper.picker.RotationStarter;
87 import com.android.wallpaper.picker.SetWallpaperDialogFragment;
88 import com.android.wallpaper.picker.SetWallpaperErrorDialogFragment;
89 import com.android.wallpaper.picker.StartRotationDialogFragment;
90 import com.android.wallpaper.picker.StartRotationErrorDialogFragment;
91 import com.android.wallpaper.picker.WallpaperInfoHelper;
92 import com.android.wallpaper.picker.WallpapersUiContainer;
93 import com.android.wallpaper.picker.individual.SetIndividualHolder.OnSetListener;
94 import com.android.wallpaper.util.DiskBasedLogger;
95 import com.android.wallpaper.util.LaunchUtils;
96 import com.android.wallpaper.util.SizeCalculator;
97 import com.android.wallpaper.widget.WallpaperInfoView;
98 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate;
99 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost;
100 
101 import com.bumptech.glide.Glide;
102 import com.bumptech.glide.MemoryCategory;
103 
104 import java.util.ArrayList;
105 import java.util.Date;
106 import java.util.List;
107 import java.util.Optional;
108 import java.util.Random;
109 import java.util.Set;
110 
111 /**
112  * Displays the Main UI for picking an individual wallpaper image.
113  */
114 public class IndividualPickerFragment extends AppbarFragment
115         implements RotationStarter, StartRotationErrorDialogFragment.Listener,
116         CurrentWallpaperBottomSheetPresenter.RefreshListener,
117         SetWallpaperErrorDialogFragment.Listener, SetWallpaperDialogFragment.Listener,
118         StartRotationDialogFragment.Listener {
119 
120     /**
121      * Position of a special tile that doesn't belong to an individual wallpaper of the category,
122      * such as "my photos" or "daily rotation".
123      */
124     static final int SPECIAL_FIXED_TILE_ADAPTER_POSITION = 0;
125     static final String ARG_CATEGORY_COLLECTION_ID = "category_collection_id";
126 
127     protected static final int MAX_CAPACITY_IN_FEWER_COLUMN_LAYOUT = 8;
128 
129     private static final String TAG = "IndividualPickerFrgmnt";
130     private static final int UNUSED_REQUEST_CODE = 1;
131     private static final String TAG_START_ROTATION_DIALOG = "start_rotation_dialog";
132     private static final String TAG_START_ROTATION_ERROR_DIALOG = "start_rotation_error_dialog";
133     private static final String PROGRESS_DIALOG_NO_TITLE = null;
134     private static final boolean PROGRESS_DIALOG_INDETERMINATE = true;
135     private static final String TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT =
136             "individual_set_wallpaper_error_dialog";
137     private static final String KEY_NIGHT_MODE = "IndividualPickerFragment.NIGHT_MODE";
138 
139     /**
140      * An interface for updating the thumbnail with the specific wallpaper.
141      */
142     public interface ThumbnailUpdater {
143         /**
144          * Updates the thumbnail with the specific wallpaper.
145          */
updateThumbnail(WallpaperInfo wallpaperInfo)146         void updateThumbnail(WallpaperInfo wallpaperInfo);
147 
148         /**
149          * Restores to the thumbnails of the wallpapers which were applied.
150          */
restoreThumbnails()151         void restoreThumbnails();
152     }
153 
154     /**
155      * An interface for receiving the destination of the new applied wallpaper.
156      */
157     public interface WallpaperDestinationCallback {
158         /**
159          * Called when the destination of the wallpaper is set.
160          *
161          * @param destination the destination which a wallpaper may be set.
162          *                    See {@link Destination} for more details.
163          */
onDestinationSet(@estination int destination)164         void onDestinationSet(@Destination int destination);
165     }
166 
167     /**
168      * The listener which will be notified when the wallpaper is selected.
169      */
170     public interface WallpaperSelectedListener {
171         /**
172          * Called when the wallpaper is selected.
173          *
174          * @param position the position of the selected wallpaper
175          */
onWallpaperSelected(int position)176         void onWallpaperSelected(int position);
177     }
178 
179     /**
180      * Interface to be implemented by a Fragment(or an Activity) hosting
181      * a {@link IndividualPickerFragment}.
182      */
183     public interface IndividualPickerFragmentHost {
184         /**
185          * Indicates if the host has toolbar to show the title. If it does, we should set the title
186          * there.
187          */
isHostToolbarShown()188         boolean isHostToolbarShown();
189 
190         /**
191          * Sets the title in the host's toolbar.
192          */
setToolbarTitle(CharSequence title)193         void setToolbarTitle(CharSequence title);
194 
195         /**
196          * Configures the menu in the toolbar.
197          *
198          * @param menuResId the resource id of the menu
199          */
setToolbarMenu(@enuRes int menuResId)200         void setToolbarMenu(@MenuRes int menuResId);
201 
202         /**
203          * Removes the menu in the toolbar.
204          */
removeToolbarMenu()205         void removeToolbarMenu();
206 
207         /**
208          * Moves to the previous fragment.
209          */
moveToPreviousFragment()210         void moveToPreviousFragment();
211     }
212 
213     WallpaperPersister mWallpaperPersister;
214     WallpaperPreferences mWallpaperPreferences;
215     WallpaperChangedNotifier mWallpaperChangedNotifier;
216     RecyclerView mImageGrid;
217     IndividualAdapter mAdapter;
218     WallpaperCategory mCategory;
219     WallpaperRotationInitializer mWallpaperRotationInitializer;
220     List<WallpaperInfo> mWallpapers;
221     Point mTileSizePx;
222     WallpapersUiContainer mWallpapersUiContainer;
223     @FormFactor
224     int mFormFactor;
225     PackageStatusNotifier mPackageStatusNotifier;
226 
227     Handler mHandler;
228     Random mRandom;
229     boolean mIsWallpapersReceived;
230 
231     WallpaperChangedNotifier.Listener mWallpaperChangedListener =
232             new WallpaperChangedNotifier.Listener() {
233         @Override
234         public void onWallpaperChanged() {
235             if (mFormFactor != FormFactorChecker.FORM_FACTOR_DESKTOP) {
236                 return;
237             }
238 
239             ViewHolder selectedViewHolder = mImageGrid.findViewHolderForAdapterPosition(
240                     mAdapter.mSelectedAdapterPosition);
241 
242             // Null remote ID => My Photos wallpaper, so deselect whatever was previously selected.
243             if (mWallpaperPreferences.getHomeWallpaperRemoteId() == null) {
244                 if (selectedViewHolder instanceof SelectableHolder) {
245                     ((SelectableHolder) selectedViewHolder).setSelectionState(
246                             SelectableHolder.SELECTION_STATE_DESELECTED);
247                 }
248             } else {
249                 mAdapter.updateSelectedTile(mAdapter.mPendingSelectedAdapterPosition);
250             }
251         }
252     };
253     PackageStatusNotifier.Listener mAppStatusListener;
254     WallpaperInfoView mWallpaperInfoView;
255     @Nullable WallpaperInfo mSelectedWallpaperInfo;
256 
257     private UserEventLogger mUserEventLogger;
258     private ProgressDialog mProgressDialog;
259     private boolean mTestingMode;
260     private CurrentWallpaperBottomSheetPresenter mCurrentWallpaperBottomSheetPresenter;
261     private SetIndividualHolder mPendingSetIndividualHolder;
262     private ContentLoadingProgressBar mLoading;
263     private CategoryProvider mCategoryProvider;
264 
265     /**
266      * Staged error dialog fragments that were unable to be shown when the activity didn't allow
267      * committing fragment transactions.
268      */
269     private SetWallpaperErrorDialogFragment mStagedSetWallpaperErrorDialogFragment;
270     private StartRotationErrorDialogFragment mStagedStartRotationErrorDialogFragment;
271 
272     private Runnable mCurrentWallpaperBottomSheetExpandedRunnable;
273 
274     /**
275      * Whether {@code mUpdateDailyWallpaperThumbRunnable} has been run at least once in this
276      * invocation of the fragment.
277      */
278     private boolean mWasUpdateRunnableRun;
279 
280     /**
281      * A Runnable which regularly updates the thumbnail for the "Daily wallpapers" tile in desktop
282      * mode.
283      */
284     private Runnable mUpdateDailyWallpaperThumbRunnable = new Runnable() {
285         @Override
286         public void run() {
287             ViewHolder viewHolder = mImageGrid.findViewHolderForAdapterPosition(
288                     SPECIAL_FIXED_TILE_ADAPTER_POSITION);
289             if (viewHolder instanceof DesktopRotationHolder) {
290                 updateDesktopDailyRotationThumbnail((DesktopRotationHolder) viewHolder);
291             } else { // viewHolder is null
292                 // If the rotation tile is unavailable (because user has scrolled down, causing the
293                 // ViewHolder to be recycled), schedule the update for some time later. Once user scrolls up
294                 // again, the ViewHolder will be re-bound and its thumbnail will be updated.
295                 mHandler.postDelayed(mUpdateDailyWallpaperThumbRunnable,
296                         DesktopRotationHolder.CROSSFADE_DURATION_MILLIS
297                                 + DesktopRotationHolder.CROSSFADE_DURATION_PAUSE_MILLIS);
298             }
299         }
300     };
301 
302     private WallpaperSetter mWallpaperSetter;
303     private WallpaperInfo mAppliedWallpaperInfo;
304     private WallpaperManager mWallpaperManager;
305     private int mWallpaperDestination;
306     private WallpaperSelectedListener mWallpaperSelectedListener;
307     private Set<String> mAppliedWallpaperIds;
308 
newInstance(String collectionId)309     public static IndividualPickerFragment newInstance(String collectionId) {
310         Bundle args = new Bundle();
311         args.putString(ARG_CATEGORY_COLLECTION_ID, collectionId);
312 
313         IndividualPickerFragment fragment = new IndividualPickerFragment();
314         fragment.setArguments(args);
315         return fragment;
316     }
317 
318     /**
319      * Highlights the applied wallpaper (if it exists) according to the destination a wallpaper
320      * would be set.
321      *
322      * @param wallpaperDestination the destination a wallpaper would be set.
323      *                             It will be either {@link Destination#DEST_HOME_SCREEN}
324      *                             or {@link Destination#DEST_LOCK_SCREEN}.
325      */
highlightAppliedWallpaper(@estination int wallpaperDestination)326     public void highlightAppliedWallpaper(@Destination int wallpaperDestination) {
327         mWallpaperDestination = wallpaperDestination;
328         if (mWallpapers != null) {
329             refreshAppliedWallpaper();
330         }
331     }
332 
updateDesktopDailyRotationThumbnail(DesktopRotationHolder holder)333     private void updateDesktopDailyRotationThumbnail(DesktopRotationHolder holder) {
334         int wallpapersIndex = mRandom.nextInt(mWallpapers.size());
335         Asset newThumbnailAsset = mWallpapers.get(wallpapersIndex).getThumbAsset(
336                 getActivity());
337         holder.updateThumbnail(newThumbnailAsset, new DrawableLoadedListener() {
338             @Override
339             public void onDrawableLoaded() {
340                 if (getActivity() == null) {
341                     return;
342                 }
343 
344                 // Schedule the next update of the thumbnail.
345                 int delayMillis = DesktopRotationHolder.CROSSFADE_DURATION_MILLIS
346                         + DesktopRotationHolder.CROSSFADE_DURATION_PAUSE_MILLIS;
347                 mHandler.postDelayed(mUpdateDailyWallpaperThumbRunnable, delayMillis);
348             }
349         });
350     }
351 
352     @Override
onCreate(Bundle savedInstanceState)353     public void onCreate(Bundle savedInstanceState) {
354         super.onCreate(savedInstanceState);
355 
356         Injector injector = InjectorProvider.getInjector();
357         Context appContext = getContext().getApplicationContext();
358         mWallpaperPreferences = injector.getPreferences(appContext);
359 
360         mWallpaperChangedNotifier = WallpaperChangedNotifier.getInstance();
361         mWallpaperChangedNotifier.registerListener(mWallpaperChangedListener);
362 
363         mWallpaperManager = WallpaperManager.getInstance(appContext);
364 
365         mFormFactor = injector.getFormFactorChecker(appContext).getFormFactor();
366 
367         mPackageStatusNotifier = injector.getPackageStatusNotifier(appContext);
368 
369         mUserEventLogger = injector.getUserEventLogger(appContext);
370 
371         mWallpaperPersister = injector.getWallpaperPersister(appContext);
372         mWallpaperSetter = new WallpaperSetter(
373                 mWallpaperPersister,
374                 injector.getPreferences(appContext),
375                 injector.getUserEventLogger(appContext),
376                 false);
377 
378         mWallpapers = new ArrayList<>();
379         mRandom = new Random();
380         mHandler = new Handler();
381 
382         // Clear Glide's cache if night-mode changed to ensure thumbnails are reloaded
383         if (savedInstanceState != null && (savedInstanceState.getInt(KEY_NIGHT_MODE)
384                 != (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK))) {
385             Glide.get(getContext()).clearMemory();
386         }
387 
388         mCategoryProvider = injector.getCategoryProvider(appContext);
389         mCategoryProvider.fetchCategories(new CategoryReceiver() {
390             @Override
391             public void onCategoryReceived(Category category) {
392                 // Do nothing.
393             }
394 
395             @Override
396             public void doneFetchingCategories() {
397                 Category category = mCategoryProvider.getCategory(
398                         getArguments().getString(ARG_CATEGORY_COLLECTION_ID));
399                 if (category != null && !(category instanceof WallpaperCategory)) {
400                     return;
401                 }
402                 mCategory = (WallpaperCategory) category;
403                 if (mCategory == null) {
404                     DiskBasedLogger.e(TAG, "Failed to find the category.", getContext());
405 
406                     // The absence of this category in the CategoryProvider indicates a broken
407                     // state, see b/38030129. Hence, finish the activity and return.
408                     getIndividualPickerFragmentHost().moveToPreviousFragment();
409                     Toast.makeText(getContext(), R.string.collection_not_exist_msg,
410                             Toast.LENGTH_SHORT).show();
411                     return;
412                 }
413                 onCategoryLoaded();
414             }
415         }, false);
416     }
417 
418 
onCategoryLoaded()419     protected void onCategoryLoaded() {
420         if (getIndividualPickerFragmentHost() == null) {
421             return;
422         }
423         if (getIndividualPickerFragmentHost().isHostToolbarShown()) {
424             getIndividualPickerFragmentHost().setToolbarTitle(mCategory.getTitle());
425         } else {
426             setTitle(mCategory.getTitle());
427         }
428         mWallpaperRotationInitializer = mCategory.getWallpaperRotationInitializer();
429         if (mToolbar != null && isRotationEnabled()) {
430             setUpToolbarMenu(R.menu.individual_picker_menu);
431         }
432         fetchWallpapers(false);
433 
434         if (mCategory.supportsThirdParty()) {
435             mAppStatusListener = (packageName, status) -> {
436                 if (status != PackageStatusNotifier.PackageStatus.REMOVED ||
437                         mCategory.containsThirdParty(packageName)) {
438                     fetchWallpapers(true);
439                 }
440             };
441             mPackageStatusNotifier.addListener(mAppStatusListener,
442                     WallpaperService.SERVICE_INTERFACE);
443         }
444     }
445 
fetchWallpapers(boolean forceReload)446     void fetchWallpapers(boolean forceReload) {
447         mWallpapers.clear();
448         mIsWallpapersReceived = false;
449         updateLoading();
450         mCategory.fetchWallpapers(getActivity().getApplicationContext(), new WallpaperReceiver() {
451             @Override
452             public void onWallpapersReceived(List<WallpaperInfo> wallpapers) {
453                 mIsWallpapersReceived = true;
454                 updateLoading();
455                 for (WallpaperInfo wallpaper : wallpapers) {
456                     mWallpapers.add(wallpaper);
457                 }
458                 maybeSetUpImageGrid();
459 
460                 // Wallpapers may load after the adapter is initialized, in which case we have
461                 // to explicitly notify that the data set has changed.
462                 if (mAdapter != null) {
463                     mAdapter.notifyDataSetChanged();
464                 }
465 
466                 if (mWallpapersUiContainer != null) {
467                     mWallpapersUiContainer.onWallpapersReady();
468                 } else {
469                     if (wallpapers.isEmpty()) {
470                         // If there are no more wallpapers and we're on phone, just finish the
471                         // Activity.
472                         Activity activity = getActivity();
473                         if (activity != null
474                                 && mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE) {
475                             activity.finish();
476                         }
477                     }
478                 }
479             }
480         }, forceReload);
481     }
482 
updateLoading()483     void updateLoading() {
484         if (mLoading == null) {
485             return;
486         }
487 
488         if (mIsWallpapersReceived) {
489             mLoading.hide();
490         } else {
491             mLoading.show();
492         }
493     }
494 
495     @Override
onSaveInstanceState(@onNull Bundle outState)496     public void onSaveInstanceState(@NonNull Bundle outState) {
497         super.onSaveInstanceState(outState);
498         outState.putInt(KEY_NIGHT_MODE,
499                 getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK);
500     }
501 
502     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)503     public View onCreateView(LayoutInflater inflater, ViewGroup container,
504                              Bundle savedInstanceState) {
505         View view = inflater.inflate(R.layout.fragment_individual_picker, container, false);
506         if (getIndividualPickerFragmentHost().isHostToolbarShown()) {
507             view.findViewById(R.id.header_bar).setVisibility(View.GONE);
508             setUpArrowEnabled(/* upArrow= */ true);
509             if (isRotationEnabled()) {
510                 getIndividualPickerFragmentHost().setToolbarMenu(R.menu.individual_picker_menu);
511             }
512         } else {
513             setUpToolbar(view);
514             if (isRotationEnabled()) {
515                 setUpToolbarMenu(R.menu.individual_picker_menu);
516             }
517             if (mCategory != null) {
518                 setTitle(mCategory.getTitle());
519             }
520         }
521 
522         mAppliedWallpaperIds = getAppliedWallpaperIds();
523 
524         mImageGrid = (RecyclerView) view.findViewById(R.id.wallpaper_grid);
525         if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
526             int gridPaddingPx = getResources().getDimensionPixelSize(R.dimen.grid_padding_desktop);
527             updateImageGridPadding(false /* addExtraBottomSpace */);
528             mImageGrid.setScrollBarSize(gridPaddingPx);
529         }
530         mLoading = view.findViewById(R.id.loading_indicator);
531         updateLoading();
532         maybeSetUpImageGrid();
533         setUpBottomSheet();
534         // For nav bar edge-to-edge effect.
535         view.setOnApplyWindowInsetsListener((v, windowInsets) -> {
536             // For status bar height.
537             v.setPadding(
538                     v.getPaddingLeft(),
539                     windowInsets.getSystemWindowInsetTop(),
540                     v.getPaddingRight(),
541                     v.getPaddingBottom());
542 
543             View gridView = v.findViewById(R.id.wallpaper_grid);
544             gridView.setPadding(
545                     gridView.getPaddingLeft(),
546                     gridView.getPaddingTop(),
547                     gridView.getPaddingRight(),
548                     windowInsets.getSystemWindowInsetBottom());
549             return windowInsets.consumeSystemWindowInsets();
550         });
551         return view;
552     }
553 
554     @Override
onClickTryAgain(@estination int unused)555     public void onClickTryAgain(@Destination int unused) {
556         if (mPendingSetIndividualHolder != null) {
557             mPendingSetIndividualHolder.setWallpaper();
558         }
559     }
560 
updateImageGridPadding(boolean addExtraBottomSpace)561     void updateImageGridPadding(boolean addExtraBottomSpace) {
562         int gridPaddingPx = getResources().getDimensionPixelSize(R.dimen.grid_padding_desktop);
563         int bottomSheetHeightPx = getResources().getDimensionPixelSize(
564                 R.dimen.current_wallpaper_bottom_sheet_layout_height);
565         int paddingBottomPx = addExtraBottomSpace ? bottomSheetHeightPx : 0;
566         // Only left and top may be set in order for the GridMarginDecoration to work properly.
567         mImageGrid.setPadding(
568                 gridPaddingPx, gridPaddingPx, 0, paddingBottomPx);
569     }
570 
getIndividualPickerFragmentHost()571     private IndividualPickerFragmentHost getIndividualPickerFragmentHost() {
572         Fragment parentFragment = getParentFragment();
573         if (parentFragment != null) {
574             return (IndividualPickerFragmentHost) parentFragment;
575         } else {
576             return (IndividualPickerFragmentHost) getActivity();
577         }
578     }
579 
maybeSetUpImageGrid()580     protected void maybeSetUpImageGrid() {
581         // Skip if mImageGrid been initialized yet
582         if (mImageGrid == null) {
583             return;
584         }
585         // Skip if category hasn't loaded yet
586         if (mCategory == null) {
587             return;
588         }
589         if (getContext() == null) {
590             return;
591         }
592 
593         // Wallpaper count could change, so we may need to change the layout(2 or 3 columns layout)
594         GridLayoutManager gridLayoutManager = (GridLayoutManager) mImageGrid.getLayoutManager();
595         boolean needUpdateLayout =
596                 gridLayoutManager != null && gridLayoutManager.getSpanCount() != getNumColumns();
597 
598         // Skip if the adapter was already created and don't need to change the layout
599         if (mAdapter != null && !needUpdateLayout) {
600             return;
601         }
602 
603         // Clear the old decoration
604         int decorationCount = mImageGrid.getItemDecorationCount();
605         for (int i = 0; i < decorationCount; i++) {
606             mImageGrid.removeItemDecorationAt(i);
607         }
608 
609         mImageGrid.addItemDecoration(new GridPaddingDecoration(getGridItemPaddingHorizontal(),
610                 getGridItemPaddingBottom()));
611         int edgePadding = getEdgePadding();
612         mImageGrid.setPadding(edgePadding, mImageGrid.getPaddingTop(), edgePadding,
613                 mImageGrid.getPaddingBottom());
614         mTileSizePx = isFewerColumnLayout()
615                 ? SizeCalculator.getFeaturedIndividualTileSize(getActivity())
616                 : SizeCalculator.getIndividualTileSize(getActivity());
617         setUpImageGrid();
618         mImageGrid.setAccessibilityDelegateCompat(
619                 new WallpaperPickerRecyclerViewAccessibilityDelegate(
620                         mImageGrid, (BottomSheetHost) getParentFragment(), getNumColumns()));
621     }
622 
isFewerColumnLayout()623     boolean isFewerColumnLayout() {
624         return mWallpapers != null && mWallpapers.size() <= MAX_CAPACITY_IN_FEWER_COLUMN_LAYOUT;
625     }
626 
getGridItemPaddingHorizontal()627     private int getGridItemPaddingHorizontal() {
628         return isFewerColumnLayout()
629                 ? getResources().getDimensionPixelSize(
630                 R.dimen.grid_item_featured_individual_padding_horizontal)
631                 : getResources().getDimensionPixelSize(
632                         R.dimen.grid_item_individual_padding_horizontal);
633     }
634 
getGridItemPaddingBottom()635     private int getGridItemPaddingBottom() {
636         return isFewerColumnLayout()
637                 ? getResources().getDimensionPixelSize(
638                 R.dimen.grid_item_featured_individual_padding_bottom)
639                 : getResources().getDimensionPixelSize(R.dimen.grid_item_individual_padding_bottom);
640     }
641 
getEdgePadding()642     private int getEdgePadding() {
643         return isFewerColumnLayout()
644                 ? getResources().getDimensionPixelSize(R.dimen.featured_wallpaper_grid_edge_space)
645                 : getResources().getDimensionPixelSize(R.dimen.wallpaper_grid_edge_space);
646     }
647 
648     /**
649      * Create the adapter and assign it to mImageGrid.
650      * Both mImageGrid and mCategory are guaranteed to not be null when this method is called.
651      */
setUpImageGrid()652     void setUpImageGrid() {
653         mAdapter = new IndividualAdapter(mWallpapers);
654         mImageGrid.setAdapter(mAdapter);
655         mImageGrid.setLayoutManager(new GridLayoutManager(getActivity(), getNumColumns()));
656     }
657 
658     /**
659      * Enables and populates the "Currently set" wallpaper BottomSheet.
660      */
setUpBottomSheet()661     void setUpBottomSheet() {
662         mImageGrid.addOnScrollListener(new OnScrollListener() {
663             @Override
664             public void onScrolled(RecyclerView recyclerView, int dx, final int dy) {
665                 if (mCurrentWallpaperBottomSheetPresenter == null) {
666                     return;
667                 }
668 
669                 if (mCurrentWallpaperBottomSheetExpandedRunnable != null) {
670                     mHandler.removeCallbacks(mCurrentWallpaperBottomSheetExpandedRunnable);
671                 }
672                 mCurrentWallpaperBottomSheetExpandedRunnable = new Runnable() {
673                     @Override
674                     public void run() {
675                         if (dy > 0) {
676                             mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(false);
677                         } else {
678                             mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(true);
679                         }
680                     }
681                 };
682                 mHandler.postDelayed(mCurrentWallpaperBottomSheetExpandedRunnable, 100);
683             }
684         });
685     }
686 
687     @Override
onResume()688     public void onResume() {
689         super.onResume();
690 
691         WallpaperPreferences preferences = InjectorProvider.getInjector()
692                 .getPreferences(getActivity());
693         preferences.setLastAppActiveTimestamp(new Date().getTime());
694 
695         // Reset Glide memory settings to a "normal" level of usage since it may have been lowered in
696         // PreviewFragment.
697         Glide.get(getActivity()).setMemoryCategory(MemoryCategory.NORMAL);
698 
699         // Show the staged 'start rotation' error dialog fragment if there is one that was unable to be
700         // shown earlier when this fragment's hosting activity didn't allow committing fragment
701         // transactions.
702         if (mStagedStartRotationErrorDialogFragment != null) {
703             mStagedStartRotationErrorDialogFragment.show(
704                     getFragmentManager(), TAG_START_ROTATION_ERROR_DIALOG);
705             mStagedStartRotationErrorDialogFragment = null;
706         }
707 
708         // Show the staged 'load wallpaper' or 'set wallpaper' error dialog fragments if there is one
709         // that was unable to be shown earlier when this fragment's hosting activity didn't allow
710         // committing fragment transactions.
711         if (mStagedSetWallpaperErrorDialogFragment != null) {
712             mStagedSetWallpaperErrorDialogFragment.show(
713                     getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT);
714             mStagedSetWallpaperErrorDialogFragment = null;
715         }
716 
717         if (shouldShowRotationTile() && mWasUpdateRunnableRun && !mWallpapers.isEmpty()) {
718             // Must be resuming from a previously stopped state, so re-schedule the update of the
719             // daily wallpapers tile thumbnail.
720             mUpdateDailyWallpaperThumbRunnable.run();
721         }
722     }
723 
724     @Override
onStop()725     public void onStop() {
726         super.onStop();
727         mHandler.removeCallbacks(mUpdateDailyWallpaperThumbRunnable);
728     }
729 
730     @Override
onDestroyView()731     public void onDestroyView() {
732         super.onDestroyView();
733         getIndividualPickerFragmentHost().removeToolbarMenu();
734     }
735 
736     @Override
onDestroy()737     public void onDestroy() {
738         super.onDestroy();
739         if (mProgressDialog != null) {
740             mProgressDialog.dismiss();
741         }
742         mWallpaperChangedNotifier.unregisterListener(mWallpaperChangedListener);
743         if (mAppStatusListener != null) {
744             mPackageStatusNotifier.removeListener(mAppStatusListener);
745         }
746         mWallpaperSetter.cleanUp();
747     }
748 
749     @Override
onStartRotationDialogDismiss(@onNull DialogInterface dialog)750     public void onStartRotationDialogDismiss(@NonNull DialogInterface dialog) {
751         // TODO(b/159310028): Refactor fragment layer to make it able to restore from config change.
752         // This is to handle config change with StartRotationDialog popup,  the StartRotationDialog
753         // still holds a reference to the destroyed Fragment and is calling
754         // onStartRotationDialogDismissed on that destroyed Fragment.
755     }
756 
757     @Override
retryStartRotation(@etworkPreference int networkPreference)758     public void retryStartRotation(@NetworkPreference int networkPreference) {
759         startRotation(networkPreference);
760     }
761 
setCurrentWallpaperBottomSheetPresenter( CurrentWallpaperBottomSheetPresenter presenter)762     public void setCurrentWallpaperBottomSheetPresenter(
763             CurrentWallpaperBottomSheetPresenter presenter) {
764         mCurrentWallpaperBottomSheetPresenter = presenter;
765     }
766 
setWallpapersUiContainer(WallpapersUiContainer uiContainer)767     public void setWallpapersUiContainer(WallpapersUiContainer uiContainer) {
768         mWallpapersUiContainer = uiContainer;
769     }
770 
setOnWallpaperSelectedListener( WallpaperSelectedListener wallpaperSelectedListener)771     public void setOnWallpaperSelectedListener(
772             WallpaperSelectedListener wallpaperSelectedListener) {
773         mWallpaperSelectedListener = wallpaperSelectedListener;
774     }
775 
776     /**
777      * Resizes the layout's height.
778      */
resizeLayout(int height)779     public void resizeLayout(int height) {
780         mImageGrid.getLayoutParams().height = height;
781         mImageGrid.requestLayout();
782     }
783 
784     /**
785      * Scrolls to the specific item.
786      *
787      * @param position the position of the item
788      */
scrollToPosition(int position)789     public void scrollToPosition(int position) {
790         ((GridLayoutManager) mImageGrid.getLayoutManager())
791                 .scrollToPositionWithOffset(position, /* offset= */ 0);
792     }
793 
794     /**
795      * Enable a test mode of operation -- in which certain UI features are disabled to allow for
796      * UI tests to run correctly. Works around issue in ProgressDialog currently where the dialog
797      * constantly keeps the UI thread alive and blocks a test forever.
798      *
799      * @param testingMode
800      */
setTestingMode(boolean testingMode)801     void setTestingMode(boolean testingMode) {
802         mTestingMode = testingMode;
803     }
804 
805     @Override
startRotation(@etworkPreference final int networkPreference)806     public void startRotation(@NetworkPreference final int networkPreference) {
807         if (!isRotationEnabled()) {
808             Log.e(TAG, "Rotation is not enabled for this category " + mCategory.getTitle());
809             return;
810         }
811 
812         // ProgressDialog endlessly updates the UI thread, keeping it from going idle which therefore
813         // causes Espresso to hang once the dialog is shown.
814         if (mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE && !mTestingMode) {
815             int themeResId;
816             if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
817                 themeResId = R.style.ProgressDialogThemePreL;
818             } else {
819                 themeResId = R.style.LightDialogTheme;
820             }
821             mProgressDialog = new ProgressDialog(getActivity(), themeResId);
822 
823             mProgressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE);
824             mProgressDialog.setMessage(
825                     getResources().getString(R.string.start_rotation_progress_message));
826             mProgressDialog.setIndeterminate(PROGRESS_DIALOG_INDETERMINATE);
827             mProgressDialog.show();
828         }
829 
830         if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
831             mAdapter.mPendingSelectedAdapterPosition = SPECIAL_FIXED_TILE_ADAPTER_POSITION;
832         }
833 
834         final Context appContext = getActivity().getApplicationContext();
835 
836         mWallpaperRotationInitializer.setFirstWallpaperInRotation(
837                 appContext,
838                 networkPreference,
839                 new Listener() {
840                     @Override
841                     public void onFirstWallpaperInRotationSet() {
842                         if (mProgressDialog != null) {
843                             mProgressDialog.dismiss();
844                         }
845 
846                         // The fragment may be detached from its containing activity if the user exits the
847                         // app before the first wallpaper image in rotation finishes downloading.
848                         Activity activity = getActivity();
849 
850 
851                         if (mWallpaperRotationInitializer.startRotation(appContext)) {
852                             if (activity != null
853                                     && mFormFactor == FormFactorChecker.FORM_FACTOR_MOBILE) {
854                                 try {
855                                     Toast.makeText(getActivity(),
856                                             R.string.wallpaper_set_successfully_message,
857                                             Toast.LENGTH_SHORT).show();
858                                 } catch (NotFoundException e) {
859                                     Log.e(TAG, "Could not show toast " + e);
860                                 }
861 
862                                 activity.setResult(Activity.RESULT_OK);
863                                 activity.finish();
864 
865                                 // Go back to launcher home.
866                                 LaunchUtils.launchHome(appContext);
867                             } else if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
868                                 mAdapter.updateSelectedTile(SPECIAL_FIXED_TILE_ADAPTER_POSITION);
869                             }
870                         } else { // Failed to start rotation.
871                             showStartRotationErrorDialog(networkPreference);
872 
873                             if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
874                                 DesktopRotationHolder rotationViewHolder =
875                                         (DesktopRotationHolder)
876                                                 mImageGrid.findViewHolderForAdapterPosition(
877                                                 SPECIAL_FIXED_TILE_ADAPTER_POSITION);
878                                 rotationViewHolder.setSelectionState(
879                                         SelectableHolder.SELECTION_STATE_DESELECTED);
880                             }
881                         }
882                     }
883 
884                     @Override
885                     public void onError() {
886                         if (mProgressDialog != null) {
887                             mProgressDialog.dismiss();
888                         }
889 
890                         showStartRotationErrorDialog(networkPreference);
891 
892                         if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
893                             DesktopRotationHolder rotationViewHolder =
894                                     (DesktopRotationHolder) mImageGrid.findViewHolderForAdapterPosition(
895                                             SPECIAL_FIXED_TILE_ADAPTER_POSITION);
896                             rotationViewHolder.setSelectionState(SelectableHolder.SELECTION_STATE_DESELECTED);
897                         }
898                     }
899                 });
900     }
901 
showStartRotationErrorDialog(@etworkPreference int networkPreference)902     private void showStartRotationErrorDialog(@NetworkPreference int networkPreference) {
903         FragmentTransactionChecker activity = (FragmentTransactionChecker) getActivity();
904         if (activity != null) {
905             StartRotationErrorDialogFragment startRotationErrorDialogFragment =
906                     StartRotationErrorDialogFragment.newInstance(networkPreference);
907             startRotationErrorDialogFragment.setTargetFragment(
908                     IndividualPickerFragment.this, UNUSED_REQUEST_CODE);
909 
910             if (activity.isSafeToCommitFragmentTransaction()) {
911                 startRotationErrorDialogFragment.show(
912                         getFragmentManager(), TAG_START_ROTATION_ERROR_DIALOG);
913             } else {
914                 mStagedStartRotationErrorDialogFragment = startRotationErrorDialogFragment;
915             }
916         }
917     }
918 
getNumColumns()919     int getNumColumns() {
920         Activity activity = getActivity();
921         if (activity == null) {
922             return 1;
923         }
924         return isFewerColumnLayout()
925                 ? SizeCalculator.getNumFeaturedIndividualColumns(activity)
926                 : SizeCalculator.getNumIndividualColumns(activity);
927     }
928 
929     /**
930      * Returns whether rotation is enabled for this category.
931      */
isRotationEnabled()932     boolean isRotationEnabled() {
933         return mWallpaperRotationInitializer != null;
934     }
935 
936     @Override
onCurrentWallpaperRefreshed()937     public void onCurrentWallpaperRefreshed() {
938         mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(true);
939     }
940 
941     @Override
onSet(int destination)942     public void onSet(int destination) {
943         if (mSelectedWallpaperInfo == null) {
944             Log.e(TAG, "Unable to set wallpaper since the selected wallpaper info is null");
945             return;
946         }
947 
948         mWallpaperPersister.setWallpaperInfoInPreview(mSelectedWallpaperInfo);
949         if (mSelectedWallpaperInfo instanceof LiveWallpaperInfo) {
950             mWallpaperSetter.setCurrentWallpaper(getActivity(), mSelectedWallpaperInfo, null,
951                     destination, 0, null, null, mSetWallpaperCallback);
952         } else {
953             mWallpaperSetter.setCurrentWallpaper(
954                     getActivity(), mSelectedWallpaperInfo, destination, mSetWallpaperCallback);
955         }
956         onWallpaperDestinationSet(destination);
957     }
958 
959     private WallpaperPersister.SetWallpaperCallback mSetWallpaperCallback =
960             new WallpaperPersister.SetWallpaperCallback() {
961                 @Override
962                 public void onSuccess(WallpaperInfo wallpaperInfo) {
963                     mWallpaperPersister.onLiveWallpaperSet();
964                     Toast.makeText(getActivity(), R.string.wallpaper_set_successfully_message,
965                             Toast.LENGTH_SHORT).show();
966                     getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
967                     getActivity().finish();
968                 }
969 
970                 @Override
971                 public void onError(@Nullable Throwable throwable) {
972                     Log.e(TAG, "Can't apply the wallpaper.");
973                 }
974             };
975 
976     @Override
onMenuItemClick(MenuItem item)977     public boolean onMenuItemClick(MenuItem item) {
978         if (item.getItemId() == R.id.daily_rotation) {
979             showRotationDialog();
980             return true;
981         }
982         return super.onMenuItemClick(item);
983     }
984 
985     /**
986      * Popups a daily rotation dialog for the uses to confirm.
987      */
showRotationDialog()988     public void showRotationDialog() {
989         DialogFragment startRotationDialogFragment = new StartRotationDialogFragment();
990         startRotationDialogFragment.setTargetFragment(
991                 IndividualPickerFragment.this, UNUSED_REQUEST_CODE);
992         startRotationDialogFragment.show(getFragmentManager(), TAG_START_ROTATION_DIALOG);
993     }
994 
995     /**
996      * Shows a "set wallpaper" error dialog with a failure message and button to try again.
997      */
showSetWallpaperErrorDialog()998     private void showSetWallpaperErrorDialog() {
999         SetWallpaperErrorDialogFragment dialogFragment = SetWallpaperErrorDialogFragment.newInstance(
1000                 R.string.set_wallpaper_error_message, WallpaperPersister.DEST_BOTH);
1001         dialogFragment.setTargetFragment(this, UNUSED_REQUEST_CODE);
1002 
1003         if (((BaseActivity) getActivity()).isSafeToCommitFragmentTransaction()) {
1004             dialogFragment.show(getFragmentManager(), TAG_SET_WALLPAPER_ERROR_DIALOG_FRAGMENT);
1005         } else {
1006             mStagedSetWallpaperErrorDialogFragment = dialogFragment;
1007         }
1008     }
1009 
updateThumbnail(WallpaperInfo selectedWallpaperInfo)1010     private void updateThumbnail(WallpaperInfo selectedWallpaperInfo) {
1011         ThumbnailUpdater thumbnailUpdater = (ThumbnailUpdater) getParentFragment();
1012         if (thumbnailUpdater == null) {
1013             return;
1014         }
1015 
1016         if (selectedWallpaperInfo != null) {
1017             thumbnailUpdater.updateThumbnail(selectedWallpaperInfo);
1018         } else {
1019             thumbnailUpdater.restoreThumbnails();
1020         }
1021     }
1022 
onWallpaperDestinationSet(int destination)1023     private void onWallpaperDestinationSet(int destination) {
1024         WallpaperDestinationCallback wallpaperDestinationCallback =
1025                 (WallpaperDestinationCallback) getParentFragment();
1026         if (wallpaperDestinationCallback == null) {
1027             return;
1028         }
1029 
1030         wallpaperDestinationCallback.onDestinationSet(destination);
1031     }
1032 
onWallpaperSelected(@ullable WallpaperInfo newSelectedWallpaperInfo, int position)1033     void onWallpaperSelected(@Nullable WallpaperInfo newSelectedWallpaperInfo,
1034                                      int position) {
1035         if (mSelectedWallpaperInfo == newSelectedWallpaperInfo) {
1036             return;
1037         }
1038         // Update current wallpaper.
1039         updateActivatedStatus(mSelectedWallpaperInfo == null
1040                 ? mAppliedWallpaperInfo : mSelectedWallpaperInfo, false);
1041         // Update new selected wallpaper.
1042         updateActivatedStatus(newSelectedWallpaperInfo == null
1043                 ? mAppliedWallpaperInfo : newSelectedWallpaperInfo, true);
1044 
1045         mSelectedWallpaperInfo = newSelectedWallpaperInfo;
1046         updateThumbnail(mSelectedWallpaperInfo);
1047         // Populate wallpaper info into view.
1048         if (mSelectedWallpaperInfo != null && mWallpaperInfoView != null) {
1049             WallpaperInfoHelper.loadExploreIntent(
1050                     getContext(),
1051                     mSelectedWallpaperInfo,
1052                     (actionLabel, exploreIntent) ->
1053                             mWallpaperInfoView.populateWallpaperInfo(
1054                                     mSelectedWallpaperInfo,
1055                                     actionLabel,
1056                                     WallpaperInfoHelper.shouldShowExploreButton(
1057                                             getContext(), exploreIntent),
1058                                     v -> onExploreClicked(exploreIntent))
1059             );
1060         }
1061 
1062         if (mWallpaperSelectedListener != null) {
1063             mWallpaperSelectedListener.onWallpaperSelected(position);
1064         }
1065     }
1066 
onExploreClicked(Intent exploreIntent)1067     private void onExploreClicked(Intent exploreIntent) {
1068         if (getContext() == null) {
1069             return;
1070         }
1071         Context context = getContext();
1072         mUserEventLogger.logActionClicked(mSelectedWallpaperInfo.getCollectionId(context),
1073                 mSelectedWallpaperInfo.getActionLabelRes(context));
1074 
1075         startActivity(exploreIntent);
1076     }
1077 
1078     // TODO: Dead code. Should remove this method in the future.
updateActivatedStatus(WallpaperInfo wallpaperInfo, boolean isActivated)1079     private void updateActivatedStatus(WallpaperInfo wallpaperInfo, boolean isActivated) {
1080         if (wallpaperInfo == null) {
1081             return;
1082         }
1083         int index = mWallpapers.indexOf(wallpaperInfo);
1084         index = (shouldShowRotationTile() || mCategory.supportsCustomPhotos())
1085                 ? index + 1 : index;
1086         ViewHolder holder = mImageGrid.findViewHolderForAdapterPosition(index);
1087         if (holder != null) {
1088             holder.itemView.setActivated(isActivated);
1089         } else {
1090             // Item is not visible, make sure the item is re-bound when it becomes visible.
1091             mAdapter.notifyItemChanged(index);
1092         }
1093     }
1094 
1095     // TODO: Dead code. Should remove this method in the future.
updateAppliedStatus(WallpaperInfo wallpaperInfo, boolean isApplied)1096     private void updateAppliedStatus(WallpaperInfo wallpaperInfo, boolean isApplied) {
1097         if (wallpaperInfo == null) {
1098             return;
1099         }
1100         int index = mWallpapers.indexOf(wallpaperInfo);
1101         index = (shouldShowRotationTile() || mCategory.supportsCustomPhotos())
1102                 ? index + 1 : index;
1103         ViewHolder holder = mImageGrid.findViewHolderForAdapterPosition(index);
1104         if (holder != null) {
1105             mAdapter.showBadge(holder, R.drawable.wallpaper_check_circle_24dp, isApplied);
1106         } else {
1107             // Item is not visible, make sure the item is re-bound when it becomes visible.
1108             mAdapter.notifyItemChanged(index);
1109         }
1110     }
1111 
refreshAppliedWallpaper()1112     private void refreshAppliedWallpaper() {
1113         // Clear the check mark and blue border(if it shows) of the old applied wallpaper.
1114         showCheckMarkAndBorderForAppliedWallpaper(false);
1115 
1116         // Update to the new applied wallpaper.
1117         String appliedWallpaperId = getAppliedWallpaperId();
1118         Optional<WallpaperInfo> wallpaperInfoOptional = mWallpapers
1119                 .stream()
1120                 .filter(wallpaper -> wallpaper.getWallpaperId() != null)
1121                 .filter(wallpaper -> wallpaper.getWallpaperId().equals(appliedWallpaperId))
1122                 .findFirst();
1123         mAppliedWallpaperInfo = wallpaperInfoOptional.orElse(null);
1124 
1125         // Set the check mark and blue border(if user doesn't select) of the new applied wallpaper.
1126         showCheckMarkAndBorderForAppliedWallpaper(true);
1127     }
1128 
getAppliedWallpaperId()1129     private String getAppliedWallpaperId() {
1130         WallpaperPreferences prefs =
1131                 InjectorProvider.getInjector().getPreferences(getContext());
1132         android.app.WallpaperInfo wallpaperInfo = mWallpaperManager.getWallpaperInfo();
1133         boolean isDestinationBoth =
1134                 mWallpaperManager.getWallpaperId(WallpaperManager.FLAG_LOCK) < 0;
1135 
1136         if (isDestinationBoth || mWallpaperDestination == WallpaperPersister.DEST_HOME_SCREEN) {
1137             return wallpaperInfo != null
1138                     ? wallpaperInfo.getServiceName() : prefs.getHomeWallpaperRemoteId();
1139         } else {
1140             return prefs.getLockWallpaperRemoteId();
1141         }
1142     }
1143 
1144     private Set<String> getAppliedWallpaperIds() {
1145         WallpaperPreferences prefs =
1146                 InjectorProvider.getInjector().getPreferences(getContext());
1147         android.app.WallpaperInfo wallpaperInfo = mWallpaperManager.getWallpaperInfo();
1148         Set<String> appliedWallpaperIds = new ArraySet<>();
1149 
1150         String homeWallpaperId = wallpaperInfo != null ? wallpaperInfo.getServiceName()
1151                 : prefs.getHomeWallpaperRemoteId();
1152         if (!TextUtils.isEmpty(homeWallpaperId)) {
1153             appliedWallpaperIds.add(homeWallpaperId);
1154         }
1155 
1156         boolean isLockWallpaperApplied =
1157                 mWallpaperManager.getWallpaperId(WallpaperManager.FLAG_LOCK) >= 0;
1158         String lockWallpaperId = prefs.getLockWallpaperRemoteId();
1159         if (isLockWallpaperApplied && !TextUtils.isEmpty(lockWallpaperId)) {
1160             appliedWallpaperIds.add(lockWallpaperId);
1161         }
1162 
1163         return appliedWallpaperIds;
1164     }
1165 
showCheckMarkAndBorderForAppliedWallpaper(boolean show)1166     private void showCheckMarkAndBorderForAppliedWallpaper(boolean show) {
1167         updateAppliedStatus(mAppliedWallpaperInfo, show);
1168         if (mSelectedWallpaperInfo == null) {
1169             updateActivatedStatus(mAppliedWallpaperInfo, show);
1170         }
1171     }
1172 
shouldShowRotationTile()1173     boolean shouldShowRotationTile() {
1174         return mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP && isRotationEnabled();
1175     }
1176 
1177     class EmptySelectionAnimator implements SelectionAnimator{
EmptySelectionAnimator()1178         EmptySelectionAnimator() {}
1179 
isSelected()1180         public boolean isSelected() {
1181             return false;
1182         }
1183 
1184         /**
1185          * Sets the UI to selected immediately with no animation.
1186          */
selectImmediately()1187         public void selectImmediately() {}
1188 
1189         /**
1190          * Sets the UI to deselected immediately with no animation.
1191          */
deselectImmediately()1192         public void deselectImmediately() {}
1193 
1194         /**
1195          * Sets the UI to selected with a smooth animation.
1196          */
animateSelected()1197         public void animateSelected() {}
1198 
1199         /**
1200          * Sets the UI to deselected with a smooth animation.
1201          */
animateDeselected()1202         public void animateDeselected() {}
1203 
1204         /**
1205          * Sets the UI to show a loading indicator.
1206          */
showLoading()1207         public void showLoading() {}
1208 
1209         /**
1210          * Sets the UI to hide the loading indicator.
1211          */
showNotLoading()1212         public void showNotLoading() {}
1213 
1214     }
1215 
1216     /**
1217      * RecyclerView Adapter subclass for the wallpaper tiles in the RecyclerView.
1218      */
1219     class IndividualAdapter extends RecyclerView.Adapter<ViewHolder> {
1220         static final int ITEM_VIEW_TYPE_ROTATION = 1;
1221         static final int ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER = 2;
1222         static final int ITEM_VIEW_TYPE_MY_PHOTOS = 3;
1223 
1224         private final List<WallpaperInfo> mWallpapers;
1225 
1226         private int mPendingSelectedAdapterPosition;
1227         private int mSelectedAdapterPosition;
1228 
IndividualAdapter(List<WallpaperInfo> wallpapers)1229         IndividualAdapter(List<WallpaperInfo> wallpapers) {
1230             mWallpapers = wallpapers;
1231             mPendingSelectedAdapterPosition = -1;
1232             mSelectedAdapterPosition = -1;
1233         }
1234 
1235         @Override
onCreateViewHolder(ViewGroup parent, int viewType)1236         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
1237             switch (viewType) {
1238                 case ITEM_VIEW_TYPE_ROTATION:
1239                     return createRotationHolder(parent);
1240                 case ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER:
1241                     return createIndividualHolder(parent);
1242                 case ITEM_VIEW_TYPE_MY_PHOTOS:
1243                     return createMyPhotosHolder(parent);
1244                 default:
1245                     Log.e(TAG, "Unsupported viewType " + viewType + " in IndividualAdapter");
1246                     return null;
1247             }
1248         }
1249 
1250         @Override
getItemViewType(int position)1251         public int getItemViewType(int position) {
1252             if (shouldShowRotationTile() && position == SPECIAL_FIXED_TILE_ADAPTER_POSITION) {
1253                 return ITEM_VIEW_TYPE_ROTATION;
1254             }
1255 
1256             // A category cannot have both a "start rotation" tile and a "my photos" tile.
1257             if (mCategory.supportsCustomPhotos()
1258                     && !isRotationEnabled()
1259                     && position == SPECIAL_FIXED_TILE_ADAPTER_POSITION) {
1260                 return ITEM_VIEW_TYPE_MY_PHOTOS;
1261             }
1262 
1263             return ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER;
1264         }
1265 
1266         @Override
onBindViewHolder(ViewHolder holder, int position)1267         public void onBindViewHolder(ViewHolder holder, int position) {
1268             int viewType = getItemViewType(position);
1269 
1270             switch (viewType) {
1271                 case ITEM_VIEW_TYPE_ROTATION:
1272                     onBindRotationHolder(holder, position);
1273                     break;
1274                 case ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER:
1275                     onBindIndividualHolder(holder, position);
1276                     break;
1277                 case ITEM_VIEW_TYPE_MY_PHOTOS:
1278                     ((MyPhotosViewHolder) holder).bind();
1279                     break;
1280                 default:
1281                     Log.e(TAG, "Unsupported viewType " + viewType + " in IndividualAdapter");
1282             }
1283         }
1284 
1285         @Override
getItemCount()1286         public int getItemCount() {
1287             return (shouldShowRotationTile() || mCategory.supportsCustomPhotos())
1288                     ? mWallpapers.size() + 1
1289                     : mWallpapers.size();
1290         }
1291 
createRotationHolder(ViewGroup parent)1292         private ViewHolder createRotationHolder(ViewGroup parent) {
1293             LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
1294             View view = layoutInflater.inflate(R.layout.grid_item_rotation_desktop, parent, false);
1295             SelectionAnimator selectionAnimator = new EmptySelectionAnimator();
1296             return new DesktopRotationHolder(getActivity(), mTileSizePx.y, view, selectionAnimator,
1297                     IndividualPickerFragment.this);
1298         }
1299 
createIndividualHolder(ViewGroup parent)1300         private ViewHolder createIndividualHolder(ViewGroup parent) {
1301             LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
1302             View view = layoutInflater.inflate(R.layout.grid_item_image, parent, false);
1303 
1304             if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
1305                 SelectionAnimator selectionAnimator = new EmptySelectionAnimator();
1306                 return new SetIndividualHolder(
1307                         getActivity(), mTileSizePx.y, view,
1308                         selectionAnimator,
1309                         new OnSetListener() {
1310                             @Override
1311                             public void onPendingWallpaperSet(int adapterPosition) {
1312                                 // Deselect and hide loading indicator for any previously pending tile.
1313                                 if (mPendingSelectedAdapterPosition != -1) {
1314                                     ViewHolder oldViewHolder = mImageGrid.findViewHolderForAdapterPosition(
1315                                             mPendingSelectedAdapterPosition);
1316                                     if (oldViewHolder instanceof SelectableHolder) {
1317                                         ((SelectableHolder) oldViewHolder).setSelectionState(
1318                                                 SelectableHolder.SELECTION_STATE_DESELECTED);
1319                                     }
1320                                 }
1321 
1322                                 if (mSelectedAdapterPosition != -1) {
1323                                     ViewHolder oldViewHolder = mImageGrid.findViewHolderForAdapterPosition(
1324                                             mSelectedAdapterPosition);
1325                                     if (oldViewHolder instanceof SelectableHolder) {
1326                                         ((SelectableHolder) oldViewHolder).setSelectionState(
1327                                                 SelectableHolder.SELECTION_STATE_DESELECTED);
1328                                     }
1329                                 }
1330 
1331                                 mPendingSelectedAdapterPosition = adapterPosition;
1332                             }
1333 
1334                             @Override
1335                             public void onWallpaperSet(int adapterPosition) {
1336                                 // No-op -- UI handles a new wallpaper being set by reacting to the
1337                                 // WallpaperChangedNotifier.
1338                             }
1339 
1340                             @Override
1341                             public void onWallpaperSetFailed(SetIndividualHolder holder) {
1342                                 showSetWallpaperErrorDialog();
1343                                 mPendingSetIndividualHolder = holder;
1344                             }
1345                         });
1346             } else { // MOBILE
1347                 return new PreviewIndividualHolder(getActivity(), mTileSizePx.y, view);
1348             }
1349         }
1350 
1351         private ViewHolder createMyPhotosHolder(ViewGroup parent) {
1352             LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
1353             View view = layoutInflater.inflate(R.layout.grid_item_my_photos, parent, false);
1354 
1355             return new MyPhotosViewHolder(getActivity(),
1356                     ((MyPhotosStarterProvider) getActivity()).getMyPhotosStarter(),
1357                     mTileSizePx.y, view);
1358         }
1359 
1360         /**
1361          * Marks the tile at the given position as selected with a visual indication. Also updates the
1362          * "currently selected" BottomSheet to reflect the newly selected tile.
1363          */
1364         private void updateSelectedTile(int newlySelectedPosition) {
1365             // Prevent multiple spinners from appearing with a user tapping several tiles in rapid
1366             // succession.
1367             if (mPendingSelectedAdapterPosition == mSelectedAdapterPosition) {
1368                 return;
1369             }
1370 
1371             if (mCurrentWallpaperBottomSheetPresenter != null) {
1372                 mCurrentWallpaperBottomSheetPresenter.refreshCurrentWallpapers(
1373                         IndividualPickerFragment.this);
1374 
1375                 if (mCurrentWallpaperBottomSheetExpandedRunnable != null) {
1376                     mHandler.removeCallbacks(mCurrentWallpaperBottomSheetExpandedRunnable);
1377                 }
1378                 mCurrentWallpaperBottomSheetExpandedRunnable = new Runnable() {
1379                     @Override
1380                     public void run() {
1381                         mCurrentWallpaperBottomSheetPresenter.setCurrentWallpapersExpanded(true);
1382                     }
1383                 };
1384                 mHandler.postDelayed(mCurrentWallpaperBottomSheetExpandedRunnable, 100);
1385             }
1386 
1387             // User may have switched to another category, thus detaching this fragment, so check here.
1388             // NOTE: We do this check after updating the current wallpaper BottomSheet so that the update
1389             // still occurs in the UI after the user selects that other category.
1390             if (getActivity() == null) {
1391                 return;
1392             }
1393 
1394             // Update the newly selected wallpaper ViewHolder and the old one so that if
1395             // selection UI state applies (desktop UI), it is updated.
1396             if (mSelectedAdapterPosition >= 0) {
1397                 ViewHolder oldViewHolder = mImageGrid.findViewHolderForAdapterPosition(
1398                         mSelectedAdapterPosition);
1399                 if (oldViewHolder instanceof SelectableHolder) {
1400                     ((SelectableHolder) oldViewHolder).setSelectionState(
1401                             SelectableHolder.SELECTION_STATE_DESELECTED);
1402                 }
1403             }
1404 
1405             // Animate selection of newly selected tile.
1406             ViewHolder newViewHolder = mImageGrid
1407                     .findViewHolderForAdapterPosition(newlySelectedPosition);
1408             if (newViewHolder instanceof SelectableHolder) {
1409                 ((SelectableHolder) newViewHolder).setSelectionState(
1410                         SelectableHolder.SELECTION_STATE_SELECTED);
1411             }
1412 
1413             mSelectedAdapterPosition = newlySelectedPosition;
1414 
1415             // If the tile was in the last row of the grid, add space below it so the user can scroll down
1416             // and up to see the BottomSheet without it fully overlapping the newly selected tile.
1417             int spanCount = ((GridLayoutManager) mImageGrid.getLayoutManager()).getSpanCount();
1418             int numRows = (int) Math.ceil((float) getItemCount() / spanCount);
1419             int rowOfNewlySelectedTile = newlySelectedPosition / spanCount;
1420             boolean isInLastRow = rowOfNewlySelectedTile == numRows - 1;
1421 
1422             updateImageGridPadding(isInLastRow /* addExtraBottomSpace */);
1423         }
1424 
1425         void onBindRotationHolder(ViewHolder holder, int position) {
1426             if (mFormFactor == FormFactorChecker.FORM_FACTOR_DESKTOP) {
1427                 String collectionId = mCategory.getCollectionId();
1428                 ((DesktopRotationHolder) holder).bind(collectionId);
1429 
1430                 if (mWallpaperPreferences.getWallpaperPresentationMode()
1431                         == WallpaperPreferences.PRESENTATION_MODE_ROTATING
1432                         && collectionId.equals(mWallpaperPreferences.getHomeWallpaperCollectionId())) {
1433                     mSelectedAdapterPosition = position;
1434                 }
1435 
1436                 if (!mWasUpdateRunnableRun && !mWallpapers.isEmpty()) {
1437                     updateDesktopDailyRotationThumbnail((DesktopRotationHolder) holder);
1438                     mWasUpdateRunnableRun = true;
1439                 }
1440             }
1441         }
1442 
1443         void onBindIndividualHolder(ViewHolder holder, int position) {
1444             int wallpaperIndex = (shouldShowRotationTile() || mCategory.supportsCustomPhotos())
1445                     ? position - 1 : position;
1446             WallpaperInfo wallpaper = mWallpapers.get(wallpaperIndex);
1447             wallpaper.computePlaceholderColor(holder.itemView.getContext());
1448             ((IndividualHolder) holder).bindWallpaper(wallpaper);
1449             boolean isWallpaperApplied = isWallpaperApplied(wallpaper);
1450 
1451             if (isWallpaperApplied) {
1452                 mSelectedAdapterPosition = position;
1453                 mAppliedWallpaperInfo = wallpaper;
1454             }
1455 
1456             CardView container = holder.itemView.findViewById(R.id.wallpaper_container);
1457             int radiusId = isFewerColumnLayout() ? R.dimen.grid_item_all_radius
1458                     : R.dimen.grid_item_all_radius_small;
1459             container.setRadius(getResources().getDimension(radiusId));
1460             showBadge(holder, R.drawable.wallpaper_check_circle_24dp, isWallpaperApplied);
1461         }
1462 
1463         protected boolean isWallpaperApplied(WallpaperInfo wallpaper) {
1464             return mAppliedWallpaperIds.contains(wallpaper.getWallpaperId());
1465         }
1466 
1467         protected void showBadge(ViewHolder holder, @DrawableRes int icon, boolean show) {
1468             ImageView badge = holder.itemView.findViewById(R.id.indicator_icon);
1469             if (show) {
1470                 final float margin = isFewerColumnLayout() ? getResources().getDimension(
1471                         R.dimen.grid_item_badge_margin) : getResources().getDimension(
1472                         R.dimen.grid_item_badge_margin_small);
1473                 final RelativeLayout.LayoutParams layoutParams =
1474                         (RelativeLayout.LayoutParams) badge.getLayoutParams();
1475                 layoutParams.setMargins(/* left= */ (int) margin, /* top= */ (int) margin,
1476                         /* right= */ (int) margin, /* bottom= */ (int) margin);
1477                 badge.setLayoutParams(layoutParams);
1478                 badge.setBackgroundResource(icon);
1479                 badge.setVisibility(View.VISIBLE);
1480             } else {
1481                 badge.setVisibility(View.GONE);
1482             }
1483         }
1484     }
1485 
1486     private class GridPaddingDecoration extends RecyclerView.ItemDecoration {
1487 
1488         private final int mPaddingHorizontal;
1489         private final int mPaddingBottom;
1490 
1491         GridPaddingDecoration(int paddingHorizontal, int paddingBottom) {
1492             mPaddingHorizontal = paddingHorizontal;
1493             mPaddingBottom = paddingBottom;
1494         }
1495 
1496         @Override
1497         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
1498                                    RecyclerView.State state) {
1499             int position = parent.getChildAdapterPosition(view);
1500             if (position >= 0) {
1501                 outRect.left = mPaddingHorizontal;
1502                 outRect.right = mPaddingHorizontal;
1503                 outRect.bottom = mPaddingBottom;
1504             }
1505         }
1506     }
1507 }
1508