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