1 /*
<lambda>null2  * Copyright (C) 2024 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 
17 package com.android.wallpaper.picker.customization.ui
18 
19 import android.graphics.Color
20 import android.graphics.Point
21 import android.os.Bundle
22 import android.view.View
23 import android.widget.FrameLayout
24 import android.widget.LinearLayout
25 import androidx.activity.OnBackPressedCallback
26 import androidx.activity.viewModels
27 import androidx.appcompat.app.AppCompatActivity
28 import androidx.constraintlayout.motion.widget.MotionLayout
29 import androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener
30 import androidx.constraintlayout.widget.ConstraintLayout
31 import androidx.constraintlayout.widget.Guideline
32 import androidx.core.view.WindowCompat
33 import androidx.core.view.doOnLayout
34 import androidx.core.view.doOnPreDraw
35 import androidx.recyclerview.widget.RecyclerView
36 import androidx.viewpager2.widget.ViewPager2
37 import com.android.wallpaper.R
38 import com.android.wallpaper.model.Screen
39 import com.android.wallpaper.model.Screen.HOME_SCREEN
40 import com.android.wallpaper.model.Screen.LOCK_SCREEN
41 import com.android.wallpaper.module.MultiPanesChecker
42 import com.android.wallpaper.picker.customization.ui.binder.CustomizationOptionsBinder
43 import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder2
44 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil
45 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil.CustomizationOption
46 import com.android.wallpaper.picker.customization.ui.view.adapter.PreviewPagerAdapter
47 import com.android.wallpaper.picker.customization.ui.view.transformer.PreviewPagerPageTransformer
48 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel2
49 import com.android.wallpaper.util.ActivityUtils
50 import dagger.hilt.android.AndroidEntryPoint
51 import javax.inject.Inject
52 
53 @AndroidEntryPoint(AppCompatActivity::class)
54 class CustomizationPickerActivity2 : Hilt_CustomizationPickerActivity2() {
55 
56     @Inject lateinit var multiPanesChecker: MultiPanesChecker
57     @Inject lateinit var customizationOptionUtil: CustomizationOptionUtil
58     @Inject lateinit var customizationOptionsBinder: CustomizationOptionsBinder
59 
60     private var fullyCollapsed = false
61 
62     private val customizationPickerViewModel: CustomizationPickerViewModel2 by viewModels()
63 
64     override fun onCreate(savedInstanceState: Bundle?) {
65         super.onCreate(savedInstanceState)
66         if (
67             multiPanesChecker.isMultiPanesEnabled(this) &&
68                 !ActivityUtils.isLaunchedFromSettingsTrampoline(intent) &&
69                 !ActivityUtils.isLaunchedFromSettingsRelated(intent)
70         ) {
71             // If the device supports multi panes, we check if the activity is launched by settings.
72             // If not, we need to start an intent to have settings launch the customization
73             // activity. In case it is a two-pane situation and the activity should be embedded in
74             // the settings app, instead of in the full screen.
75             val multiPanesIntent = multiPanesChecker.getMultiPanesIntent(intent)
76             ActivityUtils.startActivityForResultSafely(
77                 this, /* activity */
78                 multiPanesIntent,
79                 0, /* requestCode */
80             )
81             finish()
82         }
83 
84         setContentView(R.layout.activity_cusomization_picker2)
85         WindowCompat.setDecorFitsSystemWindows(window, ActivityUtils.isSUWMode(this))
86 
87         val rootView = requireViewById<MotionLayout>(R.id.picker_motion_layout)
88 
89         customizationOptionUtil.initBottomSheetContent(
90             rootView.requireViewById<FrameLayout>(R.id.customization_picker_bottom_sheet),
91             layoutInflater,
92         )
93         rootView.setTransitionListener(
94             object : EmptyTransitionListener {
95                 override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
96                     if (
97                         currentId == R.id.expanded_header_primary ||
98                             currentId == R.id.collapsed_header_primary
99                     ) {
100                         rootView.setTransition(R.id.transition_primary)
101                     }
102                 }
103             }
104         )
105 
106         initPreviewPager()
107 
108         val optionContainer = requireViewById<MotionLayout>(R.id.customization_option_container)
109         // The collapsed header height should be updated when option container's height is known
110         optionContainer.doOnPreDraw {
111             // The bottom navigation bar height
112             val navBarHeight =
113                 resources.getIdentifier("navigation_bar_height", "dimen", "android").let {
114                     if (it > 0) {
115                         resources.getDimensionPixelSize(it)
116                     } else 0
117                 }
118             val collapsedHeaderHeight = rootView.height - optionContainer.height - navBarHeight
119             if (
120                 collapsedHeaderHeight >
121                     resources.getDimensionPixelSize(
122                         R.dimen.customization_picker_preview_header_collapsed_height
123                     )
124             ) {
125                 rootView
126                     .getConstraintSet(R.id.collapsed_header_primary)
127                     ?.constrainHeight(R.id.preview_header, collapsedHeaderHeight)
128                 rootView.setTransition(R.id.transition_primary)
129             }
130         }
131 
132         val onBackPressed =
133             CustomizationPickerBinder2.bind(
134                 view = rootView,
135                 lockScreenCustomizationOptionEntries = initCustomizationOptionEntries(LOCK_SCREEN),
136                 homeScreenCustomizationOptionEntries = initCustomizationOptionEntries(HOME_SCREEN),
137                 viewModel = customizationPickerViewModel,
138                 customizationOptionsBinder = customizationOptionsBinder,
139                 lifecycleOwner = this,
140                 navigateToPrimary = {
141                     if (rootView.currentState == R.id.secondary) {
142                         rootView.transitionToState(
143                             if (fullyCollapsed) R.id.collapsed_header_primary
144                             else R.id.expanded_header_primary
145                         )
146                     }
147                 },
148                 navigateToSecondary = { screen ->
149                     if (rootView.currentState != R.id.secondary) {
150                         setCustomizePickerBottomSheetContent(rootView, screen) {
151                             fullyCollapsed = rootView.progress == 1.0f
152                             rootView.transitionToState(R.id.secondary)
153                         }
154                     }
155                 },
156             )
157 
158         onBackPressedDispatcher.addCallback(
159             object : OnBackPressedCallback(true) {
160                 override fun handleOnBackPressed() {
161                     val isOnBackPressedHandled = onBackPressed()
162                     if (!isOnBackPressedHandled) {
163                         remove()
164                         onBackPressedDispatcher.onBackPressed()
165                     }
166                 }
167             }
168         )
169     }
170 
171     override fun onDestroy() {
172         customizationOptionUtil.onDestroy()
173         super.onDestroy()
174     }
175 
176     private fun initCustomizationOptionEntries(
177         screen: Screen,
178     ): List<Pair<CustomizationOption, View>> {
179         val optionEntriesContainer =
180             requireViewById<LinearLayout>(
181                 when (screen) {
182                     LOCK_SCREEN -> R.id.lock_customization_option_container
183                     HOME_SCREEN -> R.id.home_customization_option_container
184                 }
185             )
186         val optionEntries =
187             customizationOptionUtil.getOptionEntries(screen, optionEntriesContainer, layoutInflater)
188         optionEntries.onEachIndexed { index, (option, view) ->
189             val isFirst = index == 0
190             val isLast = index == optionEntries.size - 1
191             view.setBackgroundResource(
192                 if (isFirst) R.drawable.customization_option_entry_top_background
193                 else if (isLast) R.drawable.customization_option_entry_bottom_background
194                 else R.drawable.customization_option_entry_background
195             )
196             optionEntriesContainer.addView(view)
197         }
198         return optionEntries
199     }
200 
201     private fun initPreviewPager() {
202         val pager = requireViewById<ViewPager2>(R.id.preview_pager)
203         pager.apply {
204             adapter = PreviewPagerAdapter { viewHolder, position ->
205                 viewHolder.itemView
206                     .requireViewById<View>(R.id.preview_card)
207                     .setBackgroundColor(if (position == 0) Color.BLUE else Color.CYAN)
208             }
209             // Disable over scroll
210             (getChildAt(0) as RecyclerView).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
211             // The neighboring view should be inflated when pager is rendered
212             offscreenPageLimit = 1
213             // When pager's height changes, request transform to recalculate the preview offset
214             // to make sure correct space between the previews.
215             addOnLayoutChangeListener { view, _, _, _, _, _, topWas, _, bottomWas ->
216                 val isHeightChanged = (bottomWas - topWas) != view.height
217                 if (isHeightChanged) {
218                     pager.requestTransform()
219                 }
220             }
221         }
222 
223         // Only when pager is laid out, we can get the width and set the preview's offset correctly
224         pager.doOnLayout {
225             (it as ViewPager2).apply {
226                 setPageTransformer(PreviewPagerPageTransformer(Point(width, height)))
227             }
228         }
229     }
230 
231     private fun setCustomizePickerBottomSheetContent(
232         motionContainer: MotionLayout,
233         option: CustomizationOption,
234         onComplete: () -> Unit
235     ) {
236         val view = customizationOptionUtil.getBottomSheetContent(option) ?: return
237 
238         val customizationBottomSheet =
239             requireViewById<FrameLayout>(R.id.customization_picker_bottom_sheet)
240         val guideline = requireViewById<Guideline>(R.id.preview_guideline_in_secondary_screen)
241         customizationBottomSheet.removeAllViews()
242         customizationBottomSheet.addView(view)
243 
244         view.doOnPreDraw {
245             val height = view.height
246             guideline.setGuidelineEnd(height)
247             customizationBottomSheet.translationY = 0.0f
248             customizationBottomSheet.alpha = 0.0f
249             // Update the motion container
250             motionContainer.getConstraintSet(R.id.expanded_header_primary)?.apply {
251                 setTranslationY(R.id.customization_picker_bottom_sheet, 0.0f)
252                 setAlpha(R.id.customization_picker_bottom_sheet, 0.0f)
253                 constrainHeight(
254                     R.id.customization_picker_bottom_sheet,
255                     ConstraintLayout.LayoutParams.WRAP_CONTENT
256                 )
257             }
258             motionContainer.getConstraintSet(R.id.collapsed_header_primary)?.apply {
259                 setTranslationY(R.id.customization_picker_bottom_sheet, 0.0f)
260                 setAlpha(R.id.customization_picker_bottom_sheet, 0.0f)
261                 constrainHeight(
262                     R.id.customization_picker_bottom_sheet,
263                     ConstraintLayout.LayoutParams.WRAP_CONTENT
264                 )
265             }
266             motionContainer.getConstraintSet(R.id.secondary)?.apply {
267                 setGuidelineEnd(R.id.preview_guideline_in_secondary_screen, height)
268                 setTranslationY(R.id.customization_picker_bottom_sheet, -height.toFloat())
269                 setAlpha(R.id.customization_picker_bottom_sheet, 1.0f)
270                 constrainHeight(
271                     R.id.customization_picker_bottom_sheet,
272                     ConstraintLayout.LayoutParams.WRAP_CONTENT
273                 )
274             }
275             onComplete()
276         }
277     }
278 
279     interface EmptyTransitionListener : TransitionListener {
280         override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
281             // Do nothing intended
282         }
283 
284         override fun onTransitionChange(
285             motionLayout: MotionLayout?,
286             startId: Int,
287             endId: Int,
288             progress: Float
289         ) {
290             // Do nothing intended
291         }
292 
293         override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
294             // Do nothing intended
295         }
296 
297         override fun onTransitionTrigger(
298             motionLayout: MotionLayout?,
299             triggerId: Int,
300             positive: Boolean,
301             progress: Float
302         ) {
303             // Do nothing intended
304         }
305     }
306 }
307