• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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