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