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