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.content.Context; 19 import android.os.Bundle; 20 import android.util.Log; 21 import android.view.LayoutInflater; 22 import android.view.View; 23 import android.view.ViewGroup; 24 25 import androidx.annotation.Nullable; 26 import androidx.core.widget.NestedScrollView; 27 import androidx.fragment.app.Fragment; 28 import androidx.fragment.app.FragmentManager; 29 import androidx.lifecycle.ViewModelProvider; 30 31 import com.android.settingslib.activityembedding.ActivityEmbeddingUtils; 32 import com.android.wallpaper.R; 33 import com.android.wallpaper.model.CustomizationSectionController; 34 import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController; 35 import com.android.wallpaper.model.PermissionRequester; 36 import com.android.wallpaper.model.WallpaperPreviewNavigator; 37 import com.android.wallpaper.module.CustomizationSections; 38 import com.android.wallpaper.module.FragmentFactory; 39 import com.android.wallpaper.module.Injector; 40 import com.android.wallpaper.module.InjectorProvider; 41 import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder; 42 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel; 43 import com.android.wallpaper.picker.customization.ui.viewmodel.WallpaperQuickSwitchViewModel; 44 import com.android.wallpaper.util.ActivityUtils; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.stream.Collectors; 49 50 import kotlinx.coroutines.DisposableHandle; 51 52 /** The Fragment UI for customization sections. */ 53 public class CustomizationPickerFragment extends AppbarFragment implements 54 CustomizationSectionNavigationController { 55 56 private static final String TAG = "CustomizationPickerFragment"; 57 private static final String SCROLL_POSITION_Y = "SCROLL_POSITION_Y"; 58 protected static final String KEY_IS_USE_REVAMPED_UI = "is_use_revamped_ui"; 59 private static final String KEY_START_FROM_LOCK_SCREEN = "start_from_lock_screen"; 60 private DisposableHandle mBinding; 61 62 /** Returns a new instance of {@link CustomizationPickerFragment}. */ newInstance( boolean isUseRevampedUi, boolean startFromLockScreen)63 public static CustomizationPickerFragment newInstance( 64 boolean isUseRevampedUi, 65 boolean startFromLockScreen) { 66 final CustomizationPickerFragment fragment = new CustomizationPickerFragment(); 67 final Bundle args = new Bundle(); 68 args.putBoolean(KEY_IS_USE_REVAMPED_UI, isUseRevampedUi); 69 args.putBoolean(KEY_START_FROM_LOCK_SCREEN, startFromLockScreen); 70 fragment.setArguments(args); 71 return fragment; 72 } 73 74 // Note that the section views will be displayed by the list ordering. 75 private final List<CustomizationSectionController<?>> mSectionControllers = new ArrayList<>(); 76 private NestedScrollView mNestedScrollView; 77 @Nullable 78 private Bundle mBackStackSavedInstanceState; 79 private final FragmentFactory mFragmentFactory; 80 @Nullable 81 private CustomizationPickerViewModel mViewModel; 82 CustomizationPickerFragment()83 public CustomizationPickerFragment() { 84 mFragmentFactory = InjectorProvider.getInjector().getFragmentFactory(); 85 } 86 87 @Override onCreateView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState)88 public View onCreateView(LayoutInflater inflater, ViewGroup container, 89 @Nullable Bundle savedInstanceState) { 90 final boolean shouldUseRevampedUi = shouldUseRevampedUi(); 91 final int layoutId = shouldUseRevampedUi 92 ? R.layout.toolbar_container_layout 93 : R.layout.collapsing_toolbar_container_layout; 94 final View view = inflater.inflate(layoutId, container, false); 95 if (ActivityUtils.isLaunchedFromSettingsRelated(getActivity().getIntent())) { 96 setUpToolbar(view, !ActivityEmbeddingUtils.shouldHideNavigateUpButton( 97 getActivity(), /* isSecondLayerPage= */ true)); 98 } else { 99 setUpToolbar(view, /* upArrow= */ false); 100 } 101 102 final Injector injector = InjectorProvider.getInjector(); 103 if (shouldUseRevampedUi) { 104 setContentView(view, R.layout.fragment_tabbed_customization_picker); 105 mViewModel = new ViewModelProvider( 106 this, 107 CustomizationPickerViewModel.newFactory( 108 this, 109 savedInstanceState, 110 injector.getUndoInteractor(requireContext())) 111 ).get(CustomizationPickerViewModel.class); 112 final Bundle arguments = getArguments(); 113 mViewModel.setInitialScreen( 114 arguments != null && arguments.getBoolean(KEY_START_FROM_LOCK_SCREEN)); 115 116 setUpToolbarMenu(R.menu.undoable_customization_menu); 117 final Bundle finalSavedInstanceState = savedInstanceState; 118 if (mBinding != null) { 119 mBinding.dispose(); 120 } 121 mBinding = CustomizationPickerBinder.bind( 122 view, 123 getToolbarId(), 124 mViewModel, 125 this, 126 isOnLockScreen -> filterAvailableSections( 127 getSectionControllers( 128 isOnLockScreen 129 ? CustomizationSections.Screen.LOCK_SCREEN 130 : CustomizationSections.Screen.HOME_SCREEN, 131 finalSavedInstanceState))); 132 } else { 133 setContentView(view, R.layout.fragment_customization_picker); 134 } 135 136 if (mBackStackSavedInstanceState != null) { 137 savedInstanceState = mBackStackSavedInstanceState; 138 mBackStackSavedInstanceState = null; 139 } 140 141 mNestedScrollView = view.findViewById(R.id.scroll_container); 142 143 if (!shouldUseRevampedUi) { 144 ViewGroup sectionContainer = view.findViewById(R.id.section_container); 145 sectionContainer.setOnApplyWindowInsetsListener((v, windowInsets) -> { 146 v.setPadding( 147 v.getPaddingLeft(), 148 v.getPaddingTop(), 149 v.getPaddingRight(), 150 windowInsets.getSystemWindowInsetBottom()); 151 return windowInsets.consumeSystemWindowInsets(); 152 }); 153 154 initSections(savedInstanceState); 155 mSectionControllers.forEach(controller -> 156 mNestedScrollView.post(() -> { 157 final Context context = getContext(); 158 if (context == null) { 159 Log.w(TAG, "Adding section views with null context"); 160 return; 161 } 162 sectionContainer.addView(controller.createView(context)); 163 } 164 ) 165 ); 166 167 final Bundle savedInstanceStateRef = savedInstanceState; 168 // Post it to the end of adding views to ensure restoring view state the last task. 169 view.post(() -> restoreViewState(savedInstanceStateRef)); 170 } 171 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 mNestedScrollView.post(() -> 186 mNestedScrollView.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 shouldUseRevampedUi() ? R.id.toolbar : R.id.action_bar; 199 } 200 201 @Override getToolbarColorId()202 protected int getToolbarColorId() { 203 return shouldUseRevampedUi() ? R.color.toolbar_color : android.R.color.transparent; 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 if (ActivityUtils.isLaunchedFromSettingsSearch(getActivity().getIntent())) { 215 mSectionControllers.forEach(CustomizationSectionController::onTransitionOut); 216 } 217 return super.onBackPressed(); 218 } 219 220 @Override onDestroyView()221 public void onDestroyView() { 222 // When add to back stack, #onDestroyView would be called, but #onDestroy wouldn't. So 223 // storing the state in variable to restore when back to foreground. If it's not a back 224 // stack case (i,e, config change), the variable would not be retained, see 225 // https://developer.android.com/guide/fragments/saving-state. 226 mBackStackSavedInstanceState = new Bundle(); 227 onSaveInstanceStateInternal(mBackStackSavedInstanceState); 228 229 mSectionControllers.forEach(CustomizationSectionController::release); 230 mSectionControllers.clear(); 231 super.onDestroyView(); 232 } 233 234 @Override navigateTo(Fragment fragment)235 public void navigateTo(Fragment fragment) { 236 FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); 237 fragmentManager 238 .beginTransaction() 239 .replace(R.id.fragment_container, fragment) 240 .addToBackStack(null) 241 .commit(); 242 fragmentManager.executePendingTransactions(); 243 } 244 245 @Override navigateTo(String destinationId)246 public void navigateTo(String destinationId) { 247 final Fragment fragment = mFragmentFactory.create(destinationId); 248 249 if (fragment != null) { 250 navigateTo(fragment); 251 } 252 } 253 254 /** Saves state of the fragment. */ onSaveInstanceStateInternal(Bundle savedInstanceState)255 private void onSaveInstanceStateInternal(Bundle savedInstanceState) { 256 if (mNestedScrollView != null) { 257 savedInstanceState.putInt(SCROLL_POSITION_Y, mNestedScrollView.getScrollY()); 258 } 259 mSectionControllers.forEach(c -> c.onSaveInstanceState(savedInstanceState)); 260 } 261 initSections(@ullable Bundle savedInstanceState)262 private void initSections(@Nullable Bundle savedInstanceState) { 263 // Release and clear if any. 264 mSectionControllers.forEach(CustomizationSectionController::release); 265 mSectionControllers.clear(); 266 267 mSectionControllers.addAll( 268 filterAvailableSections( 269 getSectionControllers( 270 null, 271 savedInstanceState))); 272 } 273 getSectionControllers( @ullable CustomizationSections.Screen screen, @Nullable Bundle savedInstanceState)274 private List<CustomizationSectionController<?>> getSectionControllers( 275 @Nullable CustomizationSections.Screen screen, 276 @Nullable Bundle savedInstanceState) { 277 final Injector injector = InjectorProvider.getInjector(); 278 279 WallpaperQuickSwitchViewModel wallpaperQuickSwitchViewModel = new ViewModelProvider( 280 getActivity(), 281 WallpaperQuickSwitchViewModel.newFactory( 282 this, 283 savedInstanceState, 284 injector.getWallpaperInteractor(requireContext()))) 285 .get(WallpaperQuickSwitchViewModel.class); 286 287 CustomizationSections sections = injector.getCustomizationSections(getActivity()); 288 if (screen == null) { 289 return sections.getAllSectionControllers( 290 getActivity(), 291 getViewLifecycleOwner(), 292 injector.getWallpaperColorsViewModel(), 293 getPermissionRequester(), 294 getWallpaperPreviewNavigator(), 295 this, 296 savedInstanceState, 297 injector.getDisplayUtils(getActivity())); 298 } else { 299 return sections.getRevampedUISectionControllersForScreen( 300 screen, 301 getActivity(), 302 getViewLifecycleOwner(), 303 injector.getWallpaperColorsViewModel(), 304 getPermissionRequester(), 305 getWallpaperPreviewNavigator(), 306 this, 307 savedInstanceState, 308 injector.getCurrentWallpaperInfoFactory(requireContext()), 309 injector.getDisplayUtils(getActivity()), 310 wallpaperQuickSwitchViewModel, 311 injector.getWallpaperInteractor(requireContext())); 312 } 313 } 314 315 /** Returns a filtered list containing only the available section controllers. */ filterAvailableSections( List<CustomizationSectionController<?>> controllers)316 protected List<CustomizationSectionController<?>> filterAvailableSections( 317 List<CustomizationSectionController<?>> controllers) { 318 return controllers.stream() 319 .filter(controller -> { 320 if (controller.isAvailable(getContext())) { 321 return true; 322 } else { 323 controller.release(); 324 Log.d(TAG, "Section is not available: " + controller); 325 return false; 326 } 327 }) 328 .collect(Collectors.toList()); 329 } 330 331 private PermissionRequester getPermissionRequester() { 332 return (PermissionRequester) getActivity(); 333 } 334 335 private WallpaperPreviewNavigator getWallpaperPreviewNavigator() { 336 return (WallpaperPreviewNavigator) getActivity(); 337 } 338 339 private boolean shouldUseRevampedUi() { 340 final Bundle args = getArguments(); 341 if (args != null && args.containsKey(KEY_IS_USE_REVAMPED_UI)) { 342 return args.getBoolean(KEY_IS_USE_REVAMPED_UI); 343 } else { 344 throw new IllegalStateException( 345 "Must contain KEY_IS_USE_REVAMPED_UI argument, did you instantiate directly" 346 + " instead of using the newInstance function?"); 347 } 348 } 349 } 350