1 /* 2 * Copyright (C) 2021 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; 17 18 import android.app.Activity; 19 import android.app.WallpaperManager; 20 import android.os.Bundle; 21 import android.util.Log; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 26 import androidx.activity.ComponentActivity; 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.core.content.ContextCompat; 30 import androidx.core.widget.NestedScrollView; 31 import androidx.fragment.app.Fragment; 32 import androidx.fragment.app.FragmentManager; 33 import androidx.lifecycle.ViewModelProvider; 34 import androidx.transition.Transition; 35 36 import com.android.settingslib.activityembedding.ActivityEmbeddingUtils; 37 import com.android.wallpaper.R; 38 import com.android.wallpaper.config.BaseFlags; 39 import com.android.wallpaper.model.CustomizationSectionController; 40 import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController; 41 import com.android.wallpaper.model.PermissionRequester; 42 import com.android.wallpaper.model.WallpaperPreviewNavigator; 43 import com.android.wallpaper.module.CustomizationSections; 44 import com.android.wallpaper.module.FragmentFactory; 45 import com.android.wallpaper.module.Injector; 46 import com.android.wallpaper.module.InjectorProvider; 47 import com.android.wallpaper.module.LargeScreenMultiPanesChecker; 48 import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder; 49 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel; 50 import com.android.wallpaper.util.ActivityUtils; 51 import com.android.wallpaper.util.DisplayUtils; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 import java.util.stream.Collectors; 56 57 import kotlinx.coroutines.DisposableHandle; 58 59 /** The Fragment UI for customization sections. */ 60 public class CustomizationPickerFragment extends AppbarFragment implements 61 CustomizationSectionNavigationController { 62 63 private static final String TAG = "CustomizationPickerFragment"; 64 private static final String SCROLL_POSITION_Y = "SCROLL_POSITION_Y"; 65 protected static final String KEY_IS_USE_REVAMPED_UI = "is_use_revamped_ui"; 66 private static final String KEY_START_FROM_LOCK_SCREEN = "start_from_lock_screen"; 67 private DisposableHandle mBinding; 68 69 /** Returns a new instance of {@link CustomizationPickerFragment}. */ newInstance( boolean isUseRevampedUi, boolean startFromLockScreen)70 public static CustomizationPickerFragment newInstance( 71 boolean isUseRevampedUi, 72 boolean startFromLockScreen) { 73 final CustomizationPickerFragment fragment = new CustomizationPickerFragment(); 74 final Bundle args = new Bundle(); 75 args.putBoolean(KEY_IS_USE_REVAMPED_UI, isUseRevampedUi); 76 args.putBoolean(KEY_START_FROM_LOCK_SCREEN, startFromLockScreen); 77 fragment.setArguments(args); 78 return fragment; 79 } 80 81 // Note that the section views will be displayed by the list ordering. 82 private final List<CustomizationSectionController<?>> mSectionControllers = new ArrayList<>(); 83 private NestedScrollView mHomeScrollContainer; 84 private NestedScrollView mLockScrollContainer; 85 @Nullable 86 private Bundle mBackStackSavedInstanceState; 87 private final FragmentFactory mFragmentFactory; 88 @Nullable 89 private CustomizationPickerViewModel mViewModel; 90 CustomizationPickerFragment()91 public CustomizationPickerFragment() { 92 mFragmentFactory = InjectorProvider.getInjector().getFragmentFactory(); 93 } 94 95 @Override onCreateView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState)96 public View onCreateView(LayoutInflater inflater, ViewGroup container, 97 @Nullable Bundle savedInstanceState) { 98 final boolean shouldUseRevampedUi = shouldUseRevampedUi(); 99 final int layoutId = shouldUseRevampedUi 100 ? R.layout.toolbar_container_layout 101 : R.layout.collapsing_toolbar_container_layout; 102 final View view = inflater.inflate(layoutId, container, false); 103 if (ActivityUtils.isLaunchedFromSettingsRelated(getActivity().getIntent())) { 104 setUpToolbar(view, !ActivityEmbeddingUtils.shouldHideNavigateUpButton( 105 getActivity(), /* isSecondLayerPage= */ true)); 106 } else { 107 setUpToolbar(view, /* upArrow= */ false); 108 } 109 110 final Injector injector = InjectorProvider.getInjector(); 111 setContentView(view, R.layout.fragment_tabbed_customization_picker); 112 mViewModel = new ViewModelProvider( 113 this, 114 CustomizationPickerViewModel.newFactory( 115 this, 116 savedInstanceState, 117 injector.getUndoInteractor(requireContext(), requireActivity()), 118 injector.getWallpaperInteractor(requireContext())) 119 ).get(CustomizationPickerViewModel.class); 120 final Bundle arguments = getArguments(); 121 mViewModel.setInitialScreen( 122 arguments != null && arguments.getBoolean(KEY_START_FROM_LOCK_SCREEN)); 123 124 setUpToolbarMenu(R.menu.undoable_customization_menu); 125 final Bundle finalSavedInstanceState = savedInstanceState; 126 if (mBinding != null) { 127 mBinding.dispose(); 128 } 129 final List<CustomizationSectionController<?>> lockSectionControllers = 130 getSectionControllers(CustomizationSections.Screen.LOCK_SCREEN, 131 finalSavedInstanceState); 132 final List<CustomizationSectionController<?>> homeSectionControllers = 133 getSectionControllers(CustomizationSections.Screen.HOME_SCREEN, 134 finalSavedInstanceState); 135 mSectionControllers.addAll(lockSectionControllers); 136 mSectionControllers.addAll(homeSectionControllers); 137 mBinding = CustomizationPickerBinder.bind( 138 view, 139 getToolbarId(), 140 mViewModel, 141 this, 142 isOnLockScreen -> filterAvailableSections( 143 isOnLockScreen ? lockSectionControllers : homeSectionControllers 144 )); 145 146 if (mBackStackSavedInstanceState != null) { 147 savedInstanceState = mBackStackSavedInstanceState; 148 mBackStackSavedInstanceState = null; 149 } 150 151 mHomeScrollContainer = view.findViewById(R.id.home_scroll_container); 152 mLockScrollContainer = view.findViewById(R.id.lock_scroll_container); 153 154 mHomeScrollContainer.setOnScrollChangeListener( 155 (NestedScrollView.OnScrollChangeListener) (scrollView, scrollX, scrollY, 156 oldScrollX, oldScrollY) -> { 157 if (scrollY == 0) { 158 setToolbarColor(android.R.color.transparent); 159 } else { 160 setToolbarColor(R.color.system_surface_container_highest); 161 } 162 } 163 ); 164 mLockScrollContainer.setOnScrollChangeListener( 165 (NestedScrollView.OnScrollChangeListener) (scrollView, scrollX, scrollY, 166 oldScrollX, oldScrollY) -> { 167 if (scrollY == 0) { 168 setToolbarColor(android.R.color.transparent); 169 } else { 170 setToolbarColor(R.color.system_surface_container_highest); 171 } 172 } 173 ); 174 ((ViewGroup) view).setTransitionGroup(true); 175 return view; 176 } 177 setContentView(View view, int layoutResId)178 private void setContentView(View view, int layoutResId) { 179 final ViewGroup parent = view.findViewById(R.id.content_frame); 180 if (parent != null) { 181 parent.removeAllViews(); 182 } 183 LayoutInflater.from(view.getContext()).inflate(layoutResId, parent); 184 } 185 restoreViewState(@ullable Bundle savedInstanceState)186 private void restoreViewState(@Nullable Bundle savedInstanceState) { 187 if (savedInstanceState != null) { 188 mHomeScrollContainer.post(() -> 189 mHomeScrollContainer.setScrollY(savedInstanceState.getInt(SCROLL_POSITION_Y))); 190 } 191 } 192 193 @Override onSaveInstanceState(Bundle savedInstanceState)194 public void onSaveInstanceState(Bundle savedInstanceState) { 195 onSaveInstanceStateInternal(savedInstanceState); 196 super.onSaveInstanceState(savedInstanceState); 197 } 198 199 @Override getToolbarId()200 protected int getToolbarId() { 201 return shouldUseRevampedUi() ? R.id.toolbar : R.id.action_bar; 202 } 203 204 @Override getToolbarColorId()205 protected int getToolbarColorId() { 206 return android.R.color.transparent; 207 } 208 209 @Override getToolbarTextColor()210 protected int getToolbarTextColor() { 211 return ContextCompat.getColor(requireContext(), R.color.system_on_surface); 212 } 213 214 @Override getDefaultTitle()215 public CharSequence getDefaultTitle() { 216 return getString(R.string.app_name); 217 } 218 219 @Override onBackPressed()220 public boolean onBackPressed() { 221 // TODO(b/191120122) Improve glitchy animation in Settings. 222 if (ActivityUtils.isLaunchedFromSettingsSearch(getActivity().getIntent())) { 223 mSectionControllers.forEach(CustomizationSectionController::onTransitionOut); 224 } 225 return super.onBackPressed(); 226 } 227 228 @Override onDestroyView()229 public void onDestroyView() { 230 // When add to back stack, #onDestroyView would be called, but #onDestroy wouldn't. So 231 // storing the state in variable to restore when back to foreground. If it's not a back 232 // stack case (i,e, config change), the variable would not be retained, see 233 // https://developer.android.com/guide/fragments/saving-state. 234 mBackStackSavedInstanceState = new Bundle(); 235 onSaveInstanceStateInternal(mBackStackSavedInstanceState); 236 237 mSectionControllers.forEach(CustomizationSectionController::release); 238 mSectionControllers.clear(); 239 super.onDestroyView(); 240 } 241 242 @Override navigateTo(Fragment fragment)243 public void navigateTo(Fragment fragment) { 244 prepareFragmentTransitionAnimation(); 245 FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); 246 247 boolean isPageTransitionsFeatureEnabled = 248 BaseFlags.get().isPageTransitionsFeatureEnabled(requireContext()); 249 250 fragmentManager 251 .beginTransaction() 252 .setReorderingAllowed(isPageTransitionsFeatureEnabled) 253 .replace(R.id.fragment_container, fragment) 254 .addToBackStack(null) 255 .commit(); 256 if (!isPageTransitionsFeatureEnabled) { 257 fragmentManager.executePendingTransactions(); 258 } 259 } 260 261 @Override navigateTo(String destinationId)262 public void navigateTo(String destinationId) { 263 final Fragment fragment = mFragmentFactory.create(destinationId); 264 265 if (fragment != null) { 266 navigateTo(fragment); 267 } 268 } 269 270 @Override standaloneNavigateTo(String destinationId)271 public void standaloneNavigateTo(String destinationId) { 272 final Fragment fragment = mFragmentFactory.create(destinationId); 273 prepareFragmentTransitionAnimation(); 274 275 boolean isPageTransitionsFeatureEnabled = 276 BaseFlags.get().isPageTransitionsFeatureEnabled(requireContext()); 277 278 FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); 279 fragmentManager 280 .beginTransaction() 281 .setReorderingAllowed(isPageTransitionsFeatureEnabled) 282 .replace(R.id.fragment_container, fragment) 283 .commit(); 284 if (!isPageTransitionsFeatureEnabled) { 285 fragmentManager.executePendingTransactions(); 286 } 287 } 288 prepareFragmentTransitionAnimation()289 private void prepareFragmentTransitionAnimation() { 290 Transition exitTransition = ((Transition) getExitTransition()); 291 if (exitTransition == null) return; 292 exitTransition.addListener(new Transition.TransitionListener() { 293 @Override 294 public void onTransitionStart(@NonNull Transition transition) { 295 setSurfaceViewsVisible(false); 296 } 297 298 @Override 299 public void onTransitionEnd(@NonNull Transition transition) { 300 setSurfaceViewsVisible(true); 301 } 302 303 @Override 304 public void onTransitionCancel(@NonNull Transition transition) { 305 setSurfaceViewsVisible(true); 306 // cancelling the transition breaks the preview, therefore recreating the activity 307 requireActivity().recreate(); 308 } 309 310 @Override 311 public void onTransitionPause(@NonNull Transition transition) {} 312 313 @Override 314 public void onTransitionResume(@NonNull Transition transition) {} 315 }); 316 } 317 setSurfaceViewsVisible(boolean isVisible)318 private void setSurfaceViewsVisible(boolean isVisible) { 319 mHomeScrollContainer.findViewById(R.id.preview) 320 .setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); 321 mLockScrollContainer.findViewById(R.id.preview) 322 .setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); 323 } 324 325 /** Saves state of the fragment. */ onSaveInstanceStateInternal(Bundle savedInstanceState)326 private void onSaveInstanceStateInternal(Bundle savedInstanceState) { 327 if (mHomeScrollContainer != null) { 328 savedInstanceState.putInt(SCROLL_POSITION_Y, mHomeScrollContainer.getScrollY()); 329 } 330 mSectionControllers.forEach(c -> c.onSaveInstanceState(savedInstanceState)); 331 } 332 getSectionControllers( @ullable CustomizationSections.Screen screen, @Nullable Bundle savedInstanceState)333 private List<CustomizationSectionController<?>> getSectionControllers( 334 @Nullable CustomizationSections.Screen screen, 335 @Nullable Bundle savedInstanceState) { 336 final Injector injector = InjectorProvider.getInjector(); 337 ComponentActivity activity = requireActivity(); 338 339 CustomizationSections sections = injector.getCustomizationSections(activity); 340 boolean isTwoPaneAndSmallWidth = getIsTwoPaneAndSmallWidth(activity); 341 return sections.getSectionControllersForScreen( 342 screen, 343 getActivity(), 344 getViewLifecycleOwner(), 345 injector.getWallpaperColorsViewModel(), 346 getPermissionRequester(), 347 getWallpaperPreviewNavigator(), 348 this, 349 savedInstanceState, 350 injector.getCurrentWallpaperInfoFactory(requireContext()), 351 injector.getDisplayUtils(activity), 352 mViewModel, 353 injector.getWallpaperInteractor(requireContext()), 354 WallpaperManager.getInstance(requireContext()), 355 isTwoPaneAndSmallWidth); 356 } 357 358 /** Returns a filtered list containing only the available section controllers. */ filterAvailableSections( List<CustomizationSectionController<?>> controllers)359 protected List<CustomizationSectionController<?>> filterAvailableSections( 360 List<CustomizationSectionController<?>> controllers) { 361 return controllers.stream() 362 .filter(controller -> { 363 if (controller.isAvailable(getContext())) { 364 return true; 365 } else { 366 controller.release(); 367 Log.d(TAG, "Section is not available: " + controller); 368 return false; 369 } 370 }) 371 .collect(Collectors.toList()); 372 } 373 374 private PermissionRequester getPermissionRequester() { 375 return (PermissionRequester) getActivity(); 376 } 377 378 private WallpaperPreviewNavigator getWallpaperPreviewNavigator() { 379 return (WallpaperPreviewNavigator) getActivity(); 380 } 381 382 private boolean shouldUseRevampedUi() { 383 final Bundle args = getArguments(); 384 if (args != null && args.containsKey(KEY_IS_USE_REVAMPED_UI)) { 385 return args.getBoolean(KEY_IS_USE_REVAMPED_UI); 386 } else { 387 throw new IllegalStateException( 388 "Must contain KEY_IS_USE_REVAMPED_UI argument, did you instantiate directly" 389 + " instead of using the newInstance function?"); 390 } 391 } 392 393 // TODO (b/282237387): Move wallpaper picker out of the 2-pane settings and make it a 394 // standalone app. Remove this flag when the bug is fixed. 395 private boolean getIsTwoPaneAndSmallWidth(Activity activity) { 396 DisplayUtils utils = InjectorProvider.getInjector().getDisplayUtils(requireContext()); 397 LargeScreenMultiPanesChecker multiPanesChecker = new LargeScreenMultiPanesChecker(); 398 int activityWidth = activity.getDisplay().getWidth(); 399 int widthThreshold = getResources() 400 .getDimensionPixelSize(R.dimen.two_pane_small_width_threshold); 401 return utils.isOnWallpaperDisplay(activity) 402 && multiPanesChecker.isMultiPanesEnabled(requireContext()) 403 && activityWidth <= widthThreshold; 404 } 405 } 406