• 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.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