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