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