1 /* <lambda>null2 * Copyright (C) 2022 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.app.Activity 19 import android.app.ProgressDialog 20 import android.app.WallpaperManager 21 import android.content.DialogInterface 22 import android.content.res.Configuration 23 import android.content.res.Resources 24 import android.content.res.Resources.ID_NULL 25 import android.graphics.Point 26 import android.os.Build 27 import android.os.Build.VERSION_CODES 28 import android.os.Bundle 29 import android.service.wallpaper.WallpaperService 30 import android.text.TextUtils 31 import android.util.ArraySet 32 import android.util.Log 33 import android.view.LayoutInflater 34 import android.view.MenuItem 35 import android.view.View 36 import android.view.ViewGroup 37 import android.view.WindowInsets 38 import android.widget.ImageView 39 import android.widget.RelativeLayout 40 import android.widget.TextView 41 import android.widget.Toast 42 import androidx.annotation.DrawableRes 43 import androidx.cardview.widget.CardView 44 import androidx.core.widget.ContentLoadingProgressBar 45 import androidx.fragment.app.DialogFragment 46 import androidx.lifecycle.lifecycleScope 47 import androidx.recyclerview.widget.GridLayoutManager 48 import androidx.recyclerview.widget.RecyclerView 49 import com.android.wallpaper.R 50 import com.android.wallpaper.model.Category 51 import com.android.wallpaper.model.CategoryProvider 52 import com.android.wallpaper.model.CategoryReceiver 53 import com.android.wallpaper.model.LiveWallpaperInfo 54 import com.android.wallpaper.model.WallpaperCategory 55 import com.android.wallpaper.model.WallpaperInfo 56 import com.android.wallpaper.model.WallpaperRotationInitializer 57 import com.android.wallpaper.model.WallpaperRotationInitializer.NetworkPreference 58 import com.android.wallpaper.module.InjectorProvider 59 import com.android.wallpaper.module.PackageStatusNotifier 60 import com.android.wallpaper.picker.AppbarFragment 61 import com.android.wallpaper.picker.FragmentTransactionChecker 62 import com.android.wallpaper.picker.MyPhotosStarter.MyPhotosStarterProvider 63 import com.android.wallpaper.picker.RotationStarter 64 import com.android.wallpaper.picker.StartRotationDialogFragment 65 import com.android.wallpaper.picker.StartRotationErrorDialogFragment 66 import com.android.wallpaper.util.ActivityUtils 67 import com.android.wallpaper.util.DiskBasedLogger 68 import com.android.wallpaper.util.LaunchUtils 69 import com.android.wallpaper.util.SizeCalculator 70 import com.android.wallpaper.widget.GridPaddingDecoration 71 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate 72 import com.android.wallpaper.widget.WallpaperPickerRecyclerViewAccessibilityDelegate.BottomSheetHost 73 import com.bumptech.glide.Glide 74 import com.bumptech.glide.MemoryCategory 75 import java.util.Date 76 import kotlinx.coroutines.coroutineScope 77 import kotlinx.coroutines.launch 78 79 /** Displays the Main UI for picking an individual wallpaper image. */ 80 class IndividualPickerFragment2 : 81 AppbarFragment(), 82 RotationStarter, 83 StartRotationErrorDialogFragment.Listener, 84 StartRotationDialogFragment.Listener { 85 86 companion object { 87 private const val TAG = "IndividualPickerFrag2" 88 89 /** 90 * Position of a special tile that doesn't belong to an individual wallpaper of the 91 * category, such as "my photos" or "daily rotation". 92 */ 93 private const val SPECIAL_FIXED_TILE_ADAPTER_POSITION = 0 94 95 private const val ARG_CATEGORY_COLLECTION_ID = "category_collection_id" 96 97 private const val UNUSED_REQUEST_CODE = 1 98 private const val TAG_START_ROTATION_DIALOG = "start_rotation_dialog" 99 private const val TAG_START_ROTATION_ERROR_DIALOG = "start_rotation_error_dialog" 100 private const val PROGRESS_DIALOG_INDETERMINATE = true 101 private const val KEY_NIGHT_MODE = "IndividualPickerFragment.NIGHT_MODE" 102 private const val MAX_CAPACITY_IN_FEWER_COLUMN_LAYOUT = 8 103 private val PROGRESS_DIALOG_NO_TITLE = null 104 105 fun newInstance(collectionId: String?): IndividualPickerFragment2 { 106 val args = Bundle() 107 args.putString(ARG_CATEGORY_COLLECTION_ID, collectionId) 108 val fragment = IndividualPickerFragment2() 109 fragment.arguments = args 110 return fragment 111 } 112 } 113 114 private lateinit var imageGrid: RecyclerView 115 private var adapter: IndividualAdapter? = null 116 private var category: WallpaperCategory? = null 117 private var wallpaperRotationInitializer: WallpaperRotationInitializer? = null 118 private lateinit var items: MutableList<PickerItem> 119 private var packageStatusNotifier: PackageStatusNotifier? = null 120 private var isWallpapersReceived = false 121 122 private var appStatusListener: PackageStatusNotifier.Listener? = null 123 private var progressDialog: ProgressDialog? = null 124 125 private var testingMode = false 126 private var loading: ContentLoadingProgressBar? = null 127 private var shouldReloadWallpapers = false 128 private lateinit var categoryProvider: CategoryProvider 129 private var appliedWallpaperIds: Set<String> = setOf() 130 131 /** 132 * Staged error dialog fragments that were unable to be shown when the activity didn't allow 133 * committing fragment transactions. 134 */ 135 private var stagedStartRotationErrorDialogFragment: StartRotationErrorDialogFragment? = null 136 137 private var wallpaperManager: WallpaperManager? = null 138 139 override fun onCreate(savedInstanceState: Bundle?) { 140 super.onCreate(savedInstanceState) 141 val injector = InjectorProvider.getInjector() 142 val appContext = requireContext().applicationContext 143 wallpaperManager = WallpaperManager.getInstance(appContext) 144 packageStatusNotifier = injector.getPackageStatusNotifier(appContext) 145 items = ArrayList() 146 147 // Clear Glide's cache if night-mode changed to ensure thumbnails are reloaded 148 if ( 149 savedInstanceState != null && 150 (savedInstanceState.getInt(KEY_NIGHT_MODE) != 151 resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) 152 ) { 153 Glide.get(requireContext()).clearMemory() 154 } 155 categoryProvider = injector.getCategoryProvider(appContext) 156 categoryProvider.fetchCategories( 157 object : CategoryReceiver { 158 override fun onCategoryReceived(category: Category) { 159 // Do nothing. 160 } 161 162 override fun doneFetchingCategories() { 163 val fetchedCategory = 164 categoryProvider.getCategory( 165 arguments?.getString(ARG_CATEGORY_COLLECTION_ID) 166 ) 167 if (fetchedCategory != null && fetchedCategory !is WallpaperCategory) { 168 return 169 } 170 171 if (fetchedCategory == null) { 172 DiskBasedLogger.e(TAG, "Failed to find the category.", context) 173 174 // The absence of this category in the CategoryProvider indicates a broken 175 // state, see b/38030129. Hence, finish the activity and return. 176 getIndividualPickerFragmentHost().moveToPreviousFragment() 177 Toast.makeText( 178 context, 179 R.string.collection_not_exist_msg, 180 Toast.LENGTH_SHORT 181 ) 182 .show() 183 return 184 } 185 category = fetchedCategory as WallpaperCategory 186 category?.let { onCategoryLoaded(it) } 187 } 188 }, 189 false 190 ) 191 } 192 193 fun onCategoryLoaded(category: Category) { 194 val fragmentHost = getIndividualPickerFragmentHost() 195 if (fragmentHost.isHostToolbarShown) { 196 fragmentHost.setToolbarTitle(category.title) 197 } else { 198 setTitle(category.title) 199 } 200 wallpaperRotationInitializer = category.wallpaperRotationInitializer 201 if (mToolbar != null && isRotationEnabled()) { 202 setUpToolbarMenu(R.menu.individual_picker_menu) 203 } 204 var shouldForceReload = false 205 if (category.supportsThirdParty()) { 206 shouldForceReload = true 207 } 208 fetchWallpapers(shouldForceReload) 209 registerPackageListener(category) 210 } 211 212 private fun fetchWallpapers(forceReload: Boolean) { 213 items.clear() 214 isWallpapersReceived = false 215 updateLoading() 216 val context = requireContext() 217 category?.fetchWallpapers( 218 context.applicationContext, 219 { fetchedWallpapers -> 220 isWallpapersReceived = true 221 updateLoading() 222 val byGroup = fetchedWallpapers.groupBy { it.getGroupName(context) } 223 appliedWallpaperIds = getAppliedWallpaperIds() 224 byGroup.forEach { (groupName, wallpapers) -> 225 if (!TextUtils.isEmpty(groupName)) { 226 items.add( 227 if (items.isEmpty()) { 228 PickerItem.FirstHeaderItem(groupName) 229 } else { 230 PickerItem.HeaderItem(groupName) 231 } 232 ) 233 } 234 val currentWallpaper = WallpaperManager.getInstance(context).wallpaperInfo 235 items.addAll( 236 wallpapers.map { 237 val isApplied = 238 if (it is LiveWallpaperInfo) { 239 it.isApplied(currentWallpaper) 240 } else { 241 appliedWallpaperIds.contains(it.wallpaperId) 242 } 243 PickerItem.WallpaperItem(it, isApplied) 244 } 245 ) 246 } 247 maybeSetUpImageGrid() 248 249 // Wallpapers may load after the adapter is initialized, in which case we have 250 // to explicitly notify that the data set has changed. 251 adapter?.notifyDataSetChanged() 252 if (fetchedWallpapers.isEmpty()) { 253 // If there are no more wallpapers and we're on phone, just finish the 254 // Activity. 255 val activity: Activity? = activity 256 activity?.finish() 257 } 258 }, 259 forceReload 260 ) 261 } 262 263 private fun registerPackageListener(category: Category) { 264 if (category.supportsThirdParty()) { 265 appStatusListener = 266 PackageStatusNotifier.Listener { pkgName: String?, status: Int -> 267 if ( 268 status != PackageStatusNotifier.PackageStatus.REMOVED || 269 category.containsThirdParty(pkgName) 270 ) { 271 fetchWallpapers(true) 272 } 273 } 274 packageStatusNotifier?.addListener( 275 appStatusListener, 276 WallpaperService.SERVICE_INTERFACE 277 ) 278 } 279 } 280 281 private fun updateLoading() { 282 if (isWallpapersReceived) { 283 loading?.hide() 284 } else { 285 loading?.show() 286 } 287 } 288 289 override fun onSaveInstanceState(outState: Bundle) { 290 super.onSaveInstanceState(outState) 291 outState.putInt( 292 KEY_NIGHT_MODE, 293 resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 294 ) 295 } 296 297 override fun onCreateView( 298 inflater: LayoutInflater, 299 container: ViewGroup?, 300 savedInstanceState: Bundle? 301 ): View { 302 val view: View = inflater.inflate(R.layout.fragment_individual_picker, container, false) 303 if (getIndividualPickerFragmentHost().isHostToolbarShown) { 304 view.findViewById<View>(R.id.header_bar).visibility = View.GONE 305 setUpArrowEnabled(/* upArrow= */ true) 306 if (isRotationEnabled()) { 307 getIndividualPickerFragmentHost().setToolbarMenu(R.menu.individual_picker_menu) 308 } 309 } else { 310 setUpToolbar(view) 311 if (isRotationEnabled()) { 312 setUpToolbarMenu(R.menu.individual_picker_menu) 313 } 314 setTitle(category?.title) 315 } 316 imageGrid = view.findViewById<View>(R.id.wallpaper_grid) as RecyclerView 317 loading = view.findViewById(R.id.loading_indicator) 318 updateLoading() 319 maybeSetUpImageGrid() 320 // For nav bar edge-to-edge effect. 321 imageGrid.setOnApplyWindowInsetsListener { v: View, windowInsets: WindowInsets -> 322 v.setPadding( 323 v.paddingLeft, 324 v.paddingTop, 325 v.paddingRight, 326 windowInsets.systemWindowInsetBottom 327 ) 328 windowInsets.consumeSystemWindowInsets() 329 } 330 return view 331 } 332 333 private fun getIndividualPickerFragmentHost(): 334 IndividualPickerFragment.IndividualPickerFragmentHost { 335 val parentFragment = parentFragment 336 return if (parentFragment != null) { 337 parentFragment as IndividualPickerFragment.IndividualPickerFragmentHost 338 } else { 339 activity as IndividualPickerFragment.IndividualPickerFragmentHost 340 } 341 } 342 343 private fun maybeSetUpImageGrid() { 344 // Skip if mImageGrid been initialized yet 345 if (!this::imageGrid.isInitialized) { 346 return 347 } 348 // Skip if category hasn't loaded yet 349 if (category == null) { 350 return 351 } 352 if (context == null) { 353 return 354 } 355 356 // Wallpaper count could change, so we may need to change the layout(2 or 3 columns layout) 357 val gridLayoutManager = imageGrid.layoutManager as GridLayoutManager? 358 val needUpdateLayout = gridLayoutManager?.spanCount != getNumColumns() 359 360 // Skip if the adapter was already created and don't need to change the layout 361 if (adapter != null && !needUpdateLayout) { 362 return 363 } 364 365 // Clear the old decoration 366 val decorationCount = imageGrid.itemDecorationCount 367 for (i in 0 until decorationCount) { 368 imageGrid.removeItemDecorationAt(i) 369 } 370 imageGrid.addItemDecoration( 371 GridPaddingDecoration(getGridItemPaddingHorizontal(), getGridItemPaddingBottom()) 372 ) 373 val edgePadding = getEdgePadding() 374 imageGrid.setPadding( 375 edgePadding, 376 imageGrid.paddingTop, 377 edgePadding, 378 imageGrid.paddingBottom 379 ) 380 val tileSizePx = 381 if (isFewerColumnLayout()) { 382 SizeCalculator.getFeaturedIndividualTileSize(activity!!) 383 } else { 384 SizeCalculator.getIndividualTileSize(activity!!) 385 } 386 setUpImageGrid(tileSizePx, checkNotNull(category)) 387 imageGrid.setAccessibilityDelegateCompat( 388 WallpaperPickerRecyclerViewAccessibilityDelegate( 389 imageGrid, 390 parentFragment as BottomSheetHost?, 391 getNumColumns() 392 ) 393 ) 394 } 395 396 private fun isFewerColumnLayout(): Boolean = 397 items.count { it is PickerItem.WallpaperItem } <= MAX_CAPACITY_IN_FEWER_COLUMN_LAYOUT 398 399 private fun getGridItemPaddingHorizontal(): Int { 400 return if (isFewerColumnLayout()) { 401 resources.getDimensionPixelSize( 402 R.dimen.grid_item_featured_individual_padding_horizontal 403 ) 404 } else { 405 resources.getDimensionPixelSize(R.dimen.grid_item_individual_padding_horizontal) 406 } 407 } 408 409 private fun getGridItemPaddingBottom(): Int { 410 return if (isFewerColumnLayout()) { 411 resources.getDimensionPixelSize(R.dimen.grid_item_featured_individual_padding_bottom) 412 } else { 413 resources.getDimensionPixelSize(R.dimen.grid_item_individual_padding_bottom) 414 } 415 } 416 417 private fun getEdgePadding(): Int { 418 return if (isFewerColumnLayout()) { 419 resources.getDimensionPixelSize(R.dimen.featured_wallpaper_grid_edge_space) 420 } else { 421 resources.getDimensionPixelSize(R.dimen.wallpaper_grid_edge_space) 422 } 423 } 424 425 /** 426 * Create the adapter and assign it to mImageGrid. Both mImageGrid and mCategory are guaranteed 427 * to not be null when this method is called. 428 */ 429 private fun setUpImageGrid(tileSizePx: Point, category: Category) { 430 adapter = 431 IndividualAdapter( 432 items, 433 category, 434 requireActivity(), 435 tileSizePx, 436 isRotationEnabled(), 437 isFewerColumnLayout() 438 ) 439 imageGrid.adapter = adapter 440 val gridLayoutManager = GridLayoutManager(activity, getNumColumns()) 441 gridLayoutManager.spanSizeLookup = 442 object : GridLayoutManager.SpanSizeLookup() { 443 override fun getSpanSize(position: Int): Int { 444 return when (items[position]) { 445 is PickerItem.FirstHeaderItem, 446 is PickerItem.HeaderItem -> { 447 gridLayoutManager.spanCount 448 } 449 else -> 1 450 } 451 } 452 } 453 imageGrid.layoutManager = gridLayoutManager 454 } 455 456 private suspend fun fetchWallpapersIfNeeded() { 457 coroutineScope { 458 if (isWallpapersReceived && (shouldReloadWallpapers || isAppliedWallpaperChanged())) { 459 fetchWallpapers(true) 460 } 461 } 462 } 463 464 override fun onResume() { 465 super.onResume() 466 val preferences = InjectorProvider.getInjector().getPreferences(requireActivity()) 467 preferences.lastAppActiveTimestamp = Date().time 468 469 // Reset Glide memory settings to a "normal" level of usage since it may have been lowered 470 // in PreviewFragment. 471 Glide.get(requireContext()).setMemoryCategory(MemoryCategory.NORMAL) 472 473 // Show the staged 'start rotation' error dialog fragment if there is one that was unable to 474 // be shown earlier when this fragment's hosting activity didn't allow committing fragment 475 // transactions. 476 if (isAdded) { 477 stagedStartRotationErrorDialogFragment?.show( 478 parentFragmentManager, 479 TAG_START_ROTATION_ERROR_DIALOG 480 ) 481 lifecycleScope.launch { fetchWallpapersIfNeeded() } 482 } 483 stagedStartRotationErrorDialogFragment = null 484 } 485 486 override fun onPause() { 487 shouldReloadWallpapers = category?.supportsWallpaperSetUpdates() ?: false 488 super.onPause() 489 } 490 491 override fun onDestroyView() { 492 super.onDestroyView() 493 getIndividualPickerFragmentHost().removeToolbarMenu() 494 } 495 496 override fun onDestroy() { 497 super.onDestroy() 498 progressDialog?.dismiss() 499 if (appStatusListener != null) { 500 packageStatusNotifier?.removeListener(appStatusListener) 501 } 502 } 503 504 override fun onStartRotationDialogDismiss(dialog: DialogInterface) { 505 // TODO(b/159310028): Refactor fragment layer to make it able to restore from config change. 506 // This is to handle config change with StartRotationDialog popup, the StartRotationDialog 507 // still holds a reference to the destroyed Fragment and is calling 508 // onStartRotationDialogDismissed on that destroyed Fragment. 509 } 510 511 override fun retryStartRotation(@NetworkPreference networkPreference: Int) { 512 startRotation(networkPreference) 513 } 514 515 /** 516 * Enable a test mode of operation -- in which certain UI features are disabled to allow for UI 517 * tests to run correctly. Works around issue in ProgressDialog currently where the dialog 518 * constantly keeps the UI thread alive and blocks a test forever. 519 * 520 * @param testingMode 521 */ 522 fun setTestingMode(testingMode: Boolean) { 523 this.testingMode = testingMode 524 } 525 526 override fun startRotation(@NetworkPreference networkPreference: Int) { 527 if (!isRotationEnabled()) { 528 Log.e(TAG, "Rotation is not enabled for this category " + category?.title) 529 return 530 } 531 532 // ProgressDialog endlessly updates the UI thread, keeping it from going idle which 533 // therefore causes Espresso to hang once the dialog is shown. 534 if (!testingMode) { 535 val themeResId = 536 if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) { 537 R.style.ProgressDialogThemePreL 538 } else { 539 R.style.LightDialogTheme 540 } 541 val progressDialog = ProgressDialog(activity, themeResId) 542 progressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE) 543 progressDialog.setMessage(resources.getString(R.string.start_rotation_progress_message)) 544 progressDialog.isIndeterminate = PROGRESS_DIALOG_INDETERMINATE 545 progressDialog.show() 546 this.progressDialog = progressDialog 547 } 548 val appContext = activity!!.applicationContext 549 wallpaperRotationInitializer?.setFirstWallpaperInRotation( 550 appContext, 551 networkPreference, 552 object : WallpaperRotationInitializer.Listener { 553 override fun onFirstWallpaperInRotationSet() { 554 progressDialog?.dismiss() 555 556 // The fragment may be detached from its containing activity if the user exits 557 // the app before the first wallpaper image in rotation finishes downloading. 558 val activity: Activity? = activity 559 if (wallpaperRotationInitializer!!.startRotation(appContext)) { 560 if (activity != null) { 561 try { 562 Toast.makeText( 563 activity, 564 R.string.wallpaper_set_successfully_message, 565 Toast.LENGTH_SHORT 566 ) 567 .show() 568 } catch (e: Resources.NotFoundException) { 569 Log.e(TAG, "Could not show toast $e") 570 } 571 activity.setResult(Activity.RESULT_OK) 572 activity.finish() 573 if (!ActivityUtils.isSUWMode(appContext)) { 574 // Go back to launcher home. 575 LaunchUtils.launchHome(appContext) 576 } 577 } 578 } else { // Failed to start rotation. 579 showStartRotationErrorDialog(networkPreference) 580 } 581 } 582 583 override fun onError() { 584 progressDialog?.dismiss() 585 showStartRotationErrorDialog(networkPreference) 586 } 587 } 588 ) 589 } 590 591 private fun showStartRotationErrorDialog(@NetworkPreference networkPreference: Int) { 592 val activity = activity as FragmentTransactionChecker? 593 if (activity != null) { 594 val startRotationErrorDialogFragment = 595 StartRotationErrorDialogFragment.newInstance(networkPreference) 596 startRotationErrorDialogFragment.setTargetFragment( 597 this@IndividualPickerFragment2, 598 UNUSED_REQUEST_CODE 599 ) 600 if (activity.isSafeToCommitFragmentTransaction) { 601 startRotationErrorDialogFragment.show( 602 parentFragmentManager, 603 TAG_START_ROTATION_ERROR_DIALOG 604 ) 605 } else { 606 stagedStartRotationErrorDialogFragment = startRotationErrorDialogFragment 607 } 608 } 609 } 610 611 private fun getNumColumns(): Int { 612 val activity = this.activity ?: return 1 613 return if (isFewerColumnLayout()) { 614 SizeCalculator.getNumFeaturedIndividualColumns(activity) 615 } else { 616 SizeCalculator.getNumIndividualColumns(activity) 617 } 618 } 619 620 /** Returns whether rotation is enabled for this category. */ 621 private fun isRotationEnabled() = wallpaperRotationInitializer != null 622 623 override fun onMenuItemClick(item: MenuItem): Boolean { 624 if (item.itemId == R.id.daily_rotation) { 625 showRotationDialog() 626 return true 627 } 628 return super.onMenuItemClick(item) 629 } 630 631 /** Popups a daily rotation dialog for the uses to confirm. */ 632 private fun showRotationDialog() { 633 val startRotationDialogFragment: DialogFragment = StartRotationDialogFragment() 634 startRotationDialogFragment.setTargetFragment( 635 this@IndividualPickerFragment2, 636 UNUSED_REQUEST_CODE 637 ) 638 startRotationDialogFragment.show(parentFragmentManager, TAG_START_ROTATION_DIALOG) 639 } 640 641 private fun getAppliedWallpaperIds(): Set<String> { 642 val prefs = InjectorProvider.getInjector().getPreferences(requireContext()) 643 val wallpaperInfo = wallpaperManager?.wallpaperInfo 644 val appliedWallpaperIds: MutableSet<String> = ArraySet() 645 val homeWallpaperId = 646 if (wallpaperInfo != null) { 647 wallpaperInfo.serviceName 648 } else { 649 prefs.homeWallpaperRemoteId 650 } 651 if (!TextUtils.isEmpty(homeWallpaperId)) { 652 appliedWallpaperIds.add(homeWallpaperId) 653 } 654 val isLockWallpaperApplied = 655 wallpaperManager!!.getWallpaperId(WallpaperManager.FLAG_LOCK) >= 0 656 val lockWallpaperId = prefs.lockWallpaperRemoteId 657 if (isLockWallpaperApplied && !TextUtils.isEmpty(lockWallpaperId)) { 658 appliedWallpaperIds.add(lockWallpaperId) 659 } 660 return appliedWallpaperIds 661 } 662 663 // TODO(b/277180178): Extract the check to another class for unit testing 664 private fun isAppliedWallpaperChanged(): Boolean { 665 // Reload wallpapers if the current wallpapers have changed 666 getAppliedWallpaperIds().let { 667 if (appliedWallpaperIds.isEmpty() || appliedWallpaperIds != it) { 668 return true 669 } 670 } 671 return false 672 } 673 674 sealed class PickerItem(val title: CharSequence = "") { 675 class WallpaperItem(val wallpaperInfo: WallpaperInfo, val isApplied: Boolean) : 676 PickerItem() 677 678 class HeaderItem(title: CharSequence) : PickerItem(title) 679 680 class FirstHeaderItem(title: CharSequence) : PickerItem(title) 681 } 682 683 /** RecyclerView Adapter subclass for the wallpaper tiles in the RecyclerView. */ 684 class IndividualAdapter( 685 private val items: List<PickerItem>, 686 private val category: Category, 687 private val activity: Activity, 688 private val tileSizePx: Point, 689 private val isRotationEnabled: Boolean, 690 private val isFewerColumnLayout: Boolean 691 ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { 692 companion object { 693 const val ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER = 2 694 const val ITEM_VIEW_TYPE_MY_PHOTOS = 3 695 const val ITEM_VIEW_TYPE_HEADER = 4 696 const val ITEM_VIEW_TYPE_HEADER_TOP = 5 697 } 698 699 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 700 return when (viewType) { 701 ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER -> createIndividualHolder(parent) 702 ITEM_VIEW_TYPE_MY_PHOTOS -> createMyPhotosHolder(parent) 703 ITEM_VIEW_TYPE_HEADER -> createTitleHolder(parent, /* removePaddingTop= */ false) 704 ITEM_VIEW_TYPE_HEADER_TOP -> createTitleHolder(parent, /* removePaddingTop= */ true) 705 else -> { 706 throw RuntimeException("Unsupported viewType $viewType in IndividualAdapter") 707 } 708 } 709 } 710 711 override fun getItemViewType(position: Int): Int { 712 // A category cannot have both a "start rotation" tile and a "my photos" tile. 713 return if ( 714 category.supportsCustomPhotos() && 715 !isRotationEnabled && 716 position == SPECIAL_FIXED_TILE_ADAPTER_POSITION 717 ) { 718 ITEM_VIEW_TYPE_MY_PHOTOS 719 } else { 720 when (items[position]) { 721 is PickerItem.WallpaperItem -> ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER 722 is PickerItem.HeaderItem -> ITEM_VIEW_TYPE_HEADER 723 is PickerItem.FirstHeaderItem -> ITEM_VIEW_TYPE_HEADER_TOP 724 } 725 } 726 } 727 728 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 729 when (val viewType = getItemViewType(position)) { 730 ITEM_VIEW_TYPE_INDIVIDUAL_WALLPAPER -> bindIndividualHolder(holder, position) 731 ITEM_VIEW_TYPE_MY_PHOTOS -> (holder as MyPhotosViewHolder?)!!.bind() 732 ITEM_VIEW_TYPE_HEADER, 733 ITEM_VIEW_TYPE_HEADER_TOP -> { 734 val textView = holder.itemView as TextView 735 val item = items[position] 736 textView.text = item.title 737 textView.contentDescription = item.title 738 } 739 else -> Log.e(TAG, "Unexpected viewType $viewType in IndividualAdapter") 740 } 741 } 742 743 override fun getItemCount(): Int { 744 return if (category.supportsCustomPhotos()) { 745 items.size + 1 746 } else { 747 items.size 748 } 749 } 750 751 private fun createIndividualHolder(parent: ViewGroup): RecyclerView.ViewHolder { 752 val layoutInflater = LayoutInflater.from(activity) 753 val view: View = layoutInflater.inflate(R.layout.grid_item_image, parent, false) 754 return PreviewIndividualHolder(activity, tileSizePx.y, view) 755 } 756 757 private fun createMyPhotosHolder(parent: ViewGroup): RecyclerView.ViewHolder { 758 val layoutInflater = LayoutInflater.from(activity) 759 val view: View = layoutInflater.inflate(R.layout.grid_item_my_photos, parent, false) 760 return MyPhotosViewHolder( 761 activity, 762 (activity as MyPhotosStarterProvider).myPhotosStarter, 763 tileSizePx.y, 764 view 765 ) 766 } 767 768 private fun createTitleHolder( 769 parent: ViewGroup, 770 removePaddingTop: Boolean 771 ): RecyclerView.ViewHolder { 772 val layoutInflater = LayoutInflater.from(activity) 773 val view = 774 layoutInflater.inflate(R.layout.grid_item_header, parent, /* attachToRoot= */ false) 775 if (removePaddingTop) { 776 view.setPadding( 777 view.paddingStart, 778 /* top= */ 0, 779 view.paddingEnd, 780 view.paddingBottom 781 ) 782 } 783 return object : RecyclerView.ViewHolder(view) {} 784 } 785 786 private fun bindIndividualHolder(holder: RecyclerView.ViewHolder, position: Int) { 787 val wallpaperIndex = if (category.supportsCustomPhotos()) position - 1 else position 788 val item = items[wallpaperIndex] as PickerItem.WallpaperItem 789 val wallpaper = item.wallpaperInfo 790 wallpaper.computeColorInfo(holder.itemView.context) 791 (holder as IndividualHolder).bindWallpaper(wallpaper) 792 val container = holder.itemView.findViewById<CardView>(R.id.wallpaper_container) 793 val radiusId: Int = 794 if (isFewerColumnLayout) { 795 R.dimen.grid_item_all_radius 796 } else { 797 R.dimen.grid_item_all_radius_small 798 } 799 container.radius = activity.resources.getDimension(radiusId) 800 showBadge(holder, R.drawable.wallpaper_check_circle_24dp, item.isApplied) 801 if (!item.isApplied) { 802 showBadge(holder, wallpaper.badgeDrawableRes, wallpaper.badgeDrawableRes != ID_NULL) 803 } 804 } 805 806 private fun showBadge( 807 holder: RecyclerView.ViewHolder, 808 @DrawableRes icon: Int, 809 show: Boolean 810 ) { 811 val badge = holder.itemView.findViewById<ImageView>(R.id.indicator_icon) 812 if (show) { 813 val margin = 814 if (isFewerColumnLayout) { 815 activity.resources.getDimension(R.dimen.grid_item_badge_margin) 816 } else { 817 activity.resources.getDimension(R.dimen.grid_item_badge_margin_small) 818 } 819 .toInt() 820 val layoutParams = badge.layoutParams as RelativeLayout.LayoutParams 821 layoutParams.setMargins(margin, margin, margin, margin) 822 badge.layoutParams = layoutParams 823 badge.setBackgroundResource(icon) 824 badge.visibility = View.VISIBLE 825 } else { 826 badge.visibility = View.GONE 827 } 828 } 829 } 830 } 831