1 /*
2  * Copyright (C) 2023 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.systemui.statusbar.notification.row.ui.viewbinder
18 
19 import android.view.MotionEvent
20 import android.view.View
21 import android.view.View.OnTouchListener
22 import androidx.lifecycle.Lifecycle
23 import androidx.lifecycle.repeatOnLifecycle
24 import com.android.systemui.Gefingerpoken
25 import com.android.systemui.lifecycle.repeatWhenAttached
26 import com.android.systemui.plugins.FalsingManager
27 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView
28 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
29 import kotlinx.coroutines.awaitCancellation
30 import kotlinx.coroutines.launch
31 
32 /** Binds an [ActivatableNotificationView] to its [view model][ActivatableNotificationViewModel]. */
33 object ActivatableNotificationViewBinder {
34 
35     fun bind(
36         viewModel: ActivatableNotificationViewModel,
37         view: ActivatableNotificationView,
38         falsingManager: FalsingManager,
39     ) {
40         ExpandableOutlineViewBinder.bind(viewModel, view)
41         val touchHandler = TouchHandler(view, falsingManager)
42         view.repeatWhenAttached {
43             repeatOnLifecycle(Lifecycle.State.STARTED) {
44                 launch {
45                     viewModel.isTouchable.collect { isTouchable ->
46                         touchHandler.isTouchEnabled = isTouchable
47                     }
48                 }
49                 view.registerListenersWhileAttached(touchHandler)
50             }
51         }
52     }
53 
54     private suspend fun ActivatableNotificationView.registerListenersWhileAttached(
55         touchHandler: TouchHandler,
56     ): Unit =
57         try {
58             setOnTouchListener(touchHandler)
59             setTouchHandler(touchHandler)
60             awaitCancellation()
61         } finally {
62             setTouchHandler(null)
63             setOnTouchListener(null)
64         }
65 }
66 
67 private class TouchHandler(
68     private val view: ActivatableNotificationView,
69     private val falsingManager: FalsingManager,
70 ) : Gefingerpoken, OnTouchListener {
71 
72     var isTouchEnabled = false
73 
74     override fun onTouch(v: View, ev: MotionEvent): Boolean {
75         val result = false
76         if (ev.action == MotionEvent.ACTION_UP) {
77             view.setLastActionUpTime(ev.eventTime)
78         }
79         // With a11y, just do nothing.
80         if (!isTouchEnabled) {
81             return false
82         }
83         if (ev.action == MotionEvent.ACTION_UP) {
84             // If this is a false tap, capture the even so it doesn't result in a click.
85             val falseTap: Boolean = falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
86             if (!falseTap && v is ActivatableNotificationView) {
87                 v.onTap()
88             }
89             return falseTap
90         }
91         return result
92     }
93 
94     override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false
95 
96     /** Use [onTouch] instead. */
97     override fun onTouchEvent(ev: MotionEvent): Boolean = false
98 }
99