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