/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.picker; import android.app.Activity; import android.app.WallpaperManager; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.widget.NestedScrollView; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.transition.Transition; import com.android.settingslib.activityembedding.ActivityEmbeddingUtils; import com.android.wallpaper.R; import com.android.wallpaper.config.BaseFlags; import com.android.wallpaper.model.CustomizationSectionController; import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController; import com.android.wallpaper.model.PermissionRequester; import com.android.wallpaper.model.Screen; import com.android.wallpaper.model.WallpaperPreviewNavigator; import com.android.wallpaper.module.CustomizationSections; import com.android.wallpaper.module.FragmentFactory; import com.android.wallpaper.module.Injector; import com.android.wallpaper.module.InjectorProvider; import com.android.wallpaper.module.LargeScreenMultiPanesChecker; import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder; import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel; import com.android.wallpaper.util.ActivityUtils; import com.android.wallpaper.util.DisplayUtils; import com.google.android.material.appbar.AppBarLayout; import kotlinx.coroutines.DisposableHandle; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** The Fragment UI for customization sections. */ public class CustomizationPickerFragment extends AppbarFragment implements CustomizationSectionNavigationController { private static final String TAG = "CustomizationPickerFragment"; private static final String SCROLL_POSITION_Y = "SCROLL_POSITION_Y"; private static final String KEY_START_FROM_LOCK_SCREEN = "start_from_lock_screen"; private DisposableHandle mBinding; /** Returns a new instance of {@link CustomizationPickerFragment}. */ public static CustomizationPickerFragment newInstance(boolean startFromLockScreen) { final CustomizationPickerFragment fragment = new CustomizationPickerFragment(); final Bundle args = new Bundle(); args.putBoolean(KEY_START_FROM_LOCK_SCREEN, startFromLockScreen); fragment.setArguments(args); return fragment; } // Note that the section views will be displayed by the list ordering. private final List> mSectionControllers = new ArrayList<>(); private NestedScrollView mHomeScrollContainer; private NestedScrollView mLockScrollContainer; @Nullable private Bundle mBackStackSavedInstanceState; private final FragmentFactory mFragmentFactory; @Nullable private CustomizationPickerViewModel mViewModel; public CustomizationPickerFragment() { mFragmentFactory = InjectorProvider.getInjector().getFragmentFactory(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @Nullable Bundle savedInstanceState) { final int layoutId = R.layout.toolbar_container_layout; final View view = inflater.inflate(layoutId, container, false); if (ActivityUtils.isLaunchedFromSettingsRelated(getActivity().getIntent())) { setUpToolbar(view, !ActivityEmbeddingUtils.shouldHideNavigateUpButton( getActivity(), /* isSecondLayerPage= */ true), false); } else { setUpToolbar(view, /* upArrow= */ false, false); } final Injector injector = InjectorProvider.getInjector(); setContentView(view, R.layout.fragment_tabbed_customization_picker); mViewModel = new ViewModelProvider( this, CustomizationPickerViewModel.newFactory( this, savedInstanceState, injector.getUndoInteractor(requireContext(), requireActivity()), injector.getWallpaperInteractor(requireContext()), injector.getUserEventLogger()) ).get(CustomizationPickerViewModel.class); final Bundle arguments = getArguments(); mViewModel.setInitialScreen( arguments != null && arguments.getBoolean(KEY_START_FROM_LOCK_SCREEN)); setUpToolbarMenu(R.menu.undoable_customization_menu); final Bundle finalSavedInstanceState = savedInstanceState; if (mBinding != null) { mBinding.dispose(); } final List> lockSectionControllers = getSectionControllers(Screen.LOCK_SCREEN, finalSavedInstanceState); final List> homeSectionControllers = getSectionControllers(Screen.HOME_SCREEN, finalSavedInstanceState); mSectionControllers.addAll(lockSectionControllers); mSectionControllers.addAll(homeSectionControllers); mBinding = CustomizationPickerBinder.bind( view, getToolbarId(), mViewModel, this, isOnLockScreen -> filterAvailableSections( isOnLockScreen ? lockSectionControllers : homeSectionControllers )); if (mBackStackSavedInstanceState != null) { savedInstanceState = mBackStackSavedInstanceState; mBackStackSavedInstanceState = null; } mHomeScrollContainer = view.findViewById(R.id.home_scroll_container); mLockScrollContainer = view.findViewById(R.id.lock_scroll_container); AppBarLayout appBarLayout = view.findViewById(R.id.app_bar); mHomeScrollContainer.setOnScrollChangeListener( (NestedScrollView.OnScrollChangeListener) (scrollView, scrollX, scrollY, oldScrollX, oldScrollY) -> { if (scrollY == 0) { appBarLayout.setLifted(false); } else { appBarLayout.setLifted(true); } } ); mLockScrollContainer.setOnScrollChangeListener( (NestedScrollView.OnScrollChangeListener) (scrollView, scrollX, scrollY, oldScrollX, oldScrollY) -> { if (scrollY == 0) { appBarLayout.setLifted(false); } else { appBarLayout.setLifted(true); } } ); ((ViewGroup) view).setTransitionGroup(true); return view; } private void setContentView(View view, int layoutResId) { final ViewGroup parent = view.findViewById(R.id.content_frame); if (parent != null) { parent.removeAllViews(); } LayoutInflater.from(view.getContext()).inflate(layoutResId, parent); } private void restoreViewState(@Nullable Bundle savedInstanceState) { if (savedInstanceState != null) { mHomeScrollContainer.post(() -> mHomeScrollContainer.setScrollY(savedInstanceState.getInt(SCROLL_POSITION_Y))); } } @Override public void onSaveInstanceState(Bundle savedInstanceState) { onSaveInstanceStateInternal(savedInstanceState); super.onSaveInstanceState(savedInstanceState); } @Override protected int getToolbarId() { return R.id.toolbar; } @Override protected int getToolbarTextColor() { return ContextCompat.getColor(requireContext(), R.color.system_on_surface); } @Override public CharSequence getDefaultTitle() { return getString(R.string.app_name); } @Override public boolean onBackPressed() { // TODO(b/191120122) Improve glitchy animation in Settings. if (ActivityUtils.isLaunchedFromSettingsSearch(getActivity().getIntent())) { mSectionControllers.forEach(CustomizationSectionController::onTransitionOut); } return super.onBackPressed(); } @Override public void onDestroyView() { // When add to back stack, #onDestroyView would be called, but #onDestroy wouldn't. So // storing the state in variable to restore when back to foreground. If it's not a back // stack case (i,e, config change), the variable would not be retained, see // https://developer.android.com/guide/fragments/saving-state. mBackStackSavedInstanceState = new Bundle(); onSaveInstanceStateInternal(mBackStackSavedInstanceState); mSectionControllers.forEach(CustomizationSectionController::release); mSectionControllers.clear(); super.onDestroyView(); } @Override public void navigateTo(Fragment fragment) { prepareFragmentTransitionAnimation(); FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); boolean isPageTransitionsFeatureEnabled = BaseFlags.get().isPageTransitionsFeatureEnabled(requireContext()); fragmentManager .beginTransaction() .setReorderingAllowed(isPageTransitionsFeatureEnabled) .replace(R.id.fragment_container, fragment) .addToBackStack(null) .commit(); if (!isPageTransitionsFeatureEnabled) { fragmentManager.executePendingTransactions(); } } @Override public void navigateTo(String destinationId) { final Fragment fragment = mFragmentFactory.create(destinationId); if (fragment != null) { navigateTo(fragment); } } @Override public void standaloneNavigateTo(String destinationId) { final Fragment fragment = mFragmentFactory.create(destinationId); prepareFragmentTransitionAnimation(); boolean isPageTransitionsFeatureEnabled = BaseFlags.get().isPageTransitionsFeatureEnabled(requireContext()); FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); fragmentManager .beginTransaction() .setReorderingAllowed(isPageTransitionsFeatureEnabled) .replace(R.id.fragment_container, fragment) .commit(); if (!isPageTransitionsFeatureEnabled) { fragmentManager.executePendingTransactions(); } } private void prepareFragmentTransitionAnimation() { Transition exitTransition = ((Transition) getExitTransition()); if (exitTransition == null) return; exitTransition.addListener(new Transition.TransitionListener() { @Override public void onTransitionStart(@NonNull Transition transition) { setSurfaceViewsVisible(false); } @Override public void onTransitionEnd(@NonNull Transition transition) { setSurfaceViewsVisible(true); } @Override public void onTransitionCancel(@NonNull Transition transition) { setSurfaceViewsVisible(true); // cancelling the transition breaks the preview, therefore recreating the activity requireActivity().recreate(); } @Override public void onTransitionPause(@NonNull Transition transition) {} @Override public void onTransitionResume(@NonNull Transition transition) {} }); } private void setSurfaceViewsVisible(boolean isVisible) { mHomeScrollContainer.findViewById(R.id.preview) .setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); mLockScrollContainer.findViewById(R.id.preview) .setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE); } /** Saves state of the fragment. */ private void onSaveInstanceStateInternal(Bundle savedInstanceState) { if (mHomeScrollContainer != null) { savedInstanceState.putInt(SCROLL_POSITION_Y, mHomeScrollContainer.getScrollY()); } mSectionControllers.forEach(c -> c.onSaveInstanceState(savedInstanceState)); } private List> getSectionControllers( @Nullable Screen screen, @Nullable Bundle savedInstanceState) { final Injector injector = InjectorProvider.getInjector(); ComponentActivity activity = requireActivity(); CustomizationSections sections = injector.getCustomizationSections(activity); boolean isTwoPaneAndSmallWidth = getIsTwoPaneAndSmallWidth(activity); return sections.getSectionControllersForScreen( screen, getActivity(), getViewLifecycleOwner(), injector.getWallpaperColorsRepository(), getPermissionRequester(), getWallpaperPreviewNavigator(), this, savedInstanceState, injector.getCurrentWallpaperInfoFactory(requireContext()), injector.getDisplayUtils(activity), mViewModel, injector.getWallpaperInteractor(requireContext()), WallpaperManager.getInstance(requireContext()), isTwoPaneAndSmallWidth); } /** Returns a filtered list containing only the available section controllers. */ protected List> filterAvailableSections( List> controllers) { return controllers.stream() .filter(controller -> { if (controller.isAvailable(getContext())) { return true; } else { controller.release(); Log.d(TAG, "Section is not available: " + controller); return false; } }) .collect(Collectors.toList()); } private PermissionRequester getPermissionRequester() { return (PermissionRequester) getActivity(); } private WallpaperPreviewNavigator getWallpaperPreviewNavigator() { return (WallpaperPreviewNavigator) getActivity(); } // TODO (b/282237387): Move wallpaper picker out of the 2-pane settings and make it a // standalone app. Remove this flag when the bug is fixed. private boolean getIsTwoPaneAndSmallWidth(Activity activity) { DisplayUtils utils = InjectorProvider.getInjector().getDisplayUtils(requireContext()); LargeScreenMultiPanesChecker multiPanesChecker = new LargeScreenMultiPanesChecker(); int activityWidth = activity.getDisplay().getWidth(); int widthThreshold = getResources() .getDimensionPixelSize(R.dimen.two_pane_small_width_threshold); return utils.isOnWallpaperDisplay(activity) && multiPanesChecker.isMultiPanesEnabled(requireContext()) && activityWidth <= widthThreshold; } }