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