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