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.wrapper
18 
19 import android.app.PendingIntent
20 import android.app.PendingIntent.CancelListener
21 import android.content.Intent
22 import android.testing.TestableLooper
23 import android.testing.TestableLooper.RunWithLooper
24 import android.testing.ViewUtils
25 import android.view.LayoutInflater
26 import android.view.View
27 import android.view.ViewGroup
28 import android.widget.FrameLayout
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.SmallTest
31 import com.android.internal.R
32 import com.android.systemui.SysuiTestCase
33 import com.android.systemui.TestUiOffloadThread
34 import com.android.systemui.UiOffloadThread
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
36 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
37 import com.android.systemui.statusbar.notification.row.wrapper.NotificationTemplateViewWrapper.ActionPendingIntentCancellationHandler
38 import com.android.systemui.util.leak.ReferenceTestUtils.waitForCondition
39 import com.google.common.truth.Truth.assertThat
40 import org.junit.Before
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 import org.mockito.ArgumentCaptor
44 import org.mockito.Mockito
45 import org.mockito.Mockito.times
46 import org.mockito.Mockito.verify
47 
48 @SmallTest
49 @RunWith(AndroidJUnit4::class)
50 @RunWithLooper
51 class NotificationTemplateViewWrapperTest : SysuiTestCase() {
52 
53     private lateinit var helper: NotificationTestHelper
54 
55     private lateinit var root: ViewGroup
56     private lateinit var view: ViewGroup
57     private lateinit var row: ExpandableNotificationRow
58     private lateinit var actions: ViewGroup
59 
60     private lateinit var looper: TestableLooper
61 
62     @Before
setUpnull63     fun setUp() {
64         looper = TestableLooper.get(this)
65         allowTestableLooperAsMainThread()
66         // Use main thread instead of UI offload thread to fix flakes.
67         mDependency.injectTestDependency(
68             UiOffloadThread::class.java,
69             TestUiOffloadThread(looper.looper)
70         )
71 
72         helper = NotificationTestHelper(mContext, mDependency)
73         row = helper.createRow()
74         // Some code in the view iterates through parents so we need some extra containers around
75         // it.
76         root = FrameLayout(mContext)
77         val root2 = FrameLayout(mContext)
78         root.addView(root2)
79         view =
80             (LayoutInflater.from(mContext)
81                 .inflate(R.layout.notification_template_material_big_text, root2) as ViewGroup)
82         actions = view.findViewById(R.id.actions)!!
83         ViewUtils.attachView(root)
84     }
85 
86     @Test
noActionsPresent_noCrashnull87     fun noActionsPresent_noCrash() {
88         view.removeView(actions)
89         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
90         wrapper.onContentUpdated(row)
91     }
92 
93     @Test
actionPendingIntentCancelled_actionDisablednull94     fun actionPendingIntentCancelled_actionDisabled() {
95         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
96         val action1 = createActionWithPendingIntent()
97         val action2 = createActionWithPendingIntent()
98         val action3 = createActionWithPendingIntent()
99         wrapper.onContentUpdated(row)
100 
101         val pi3 = getPendingIntent(action3)
102         pi3.cancel()
103 
104         waitForActionDisabled(action3)
105         assertThat(action1.isEnabled).isTrue()
106         assertThat(action2.isEnabled).isTrue()
107         assertThat(action3.isEnabled).isFalse()
108         assertThat(wrapper.mCancelledPendingIntents)
109             .doesNotContain(getPendingIntent(action1).hashCode())
110         assertThat(wrapper.mCancelledPendingIntents)
111             .doesNotContain(getPendingIntent(action2).hashCode())
112         assertThat(wrapper.mCancelledPendingIntents).contains(pi3.hashCode())
113     }
114 
115     @Test
newActionWithSamePendingIntentPosted_actionDisablednull116     fun newActionWithSamePendingIntentPosted_actionDisabled() {
117         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
118         val action = createActionWithPendingIntent()
119         wrapper.onContentUpdated(row)
120 
121         // Cancel the intent and check action is now false.
122         val pi = getPendingIntent(action)
123         pi.cancel()
124 
125         waitForActionDisabled(action)
126         assertThat(action.isEnabled).isFalse()
127 
128         // Create a NEW action and make sure that one will also be cancelled with same PI.
129         actions.removeView(action)
130         val newAction = createActionWithPendingIntent(pi)
131         wrapper.onContentUpdated(row)
132         looper.processAllMessages() // Wait for listener callbacks to execute
133 
134         assertThat(newAction.isEnabled).isFalse()
135         assertThat(wrapper.mCancelledPendingIntents).containsExactly(pi.hashCode())
136     }
137 
138     @Test
twoActionsWithSameCancelledIntent_bothActionsDisablednull139     fun twoActionsWithSameCancelledIntent_bothActionsDisabled() {
140         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
141         val action1 = createActionWithPendingIntent()
142         val action2 = createActionWithPendingIntent()
143         val action3 = createActionWithPendingIntent(getPendingIntent(action2))
144         wrapper.onContentUpdated(row)
145         looper.processAllMessages()
146 
147         val pi = getPendingIntent(action2)
148         pi.cancel()
149 
150         waitForActionDisabled(action2)
151         waitForActionDisabled(action3)
152         assertThat(action1.isEnabled).isTrue()
153         assertThat(action2.isEnabled).isFalse()
154         assertThat(action3.isEnabled).isFalse()
155     }
156 
157     @Test
actionPendingIntentCancelled_whileDetached_actionDisablednull158     fun actionPendingIntentCancelled_whileDetached_actionDisabled() {
159         ViewUtils.detachView(root)
160         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
161         val action = createActionWithPendingIntent()
162         wrapper.onContentUpdated(row)
163         getPendingIntent(action).cancel()
164         looper.processAllMessages()
165 
166         ViewUtils.attachView(root)
167         looper.processAllMessages()
168 
169         waitForActionDisabled(action)
170         assertThat(action.isEnabled).isFalse()
171     }
172 
173     @Test
actionViewDetached_pendingIntentListenersDeregisterednull174     fun actionViewDetached_pendingIntentListenersDeregistered() {
175         val pi =
176             PendingIntent.getActivity(
177                 mContext,
178                 System.currentTimeMillis().toInt(),
179                 Intent(Intent.ACTION_VIEW),
180                 PendingIntent.FLAG_IMMUTABLE
181             )
182         val spy = Mockito.spy(pi)
183         createActionWithPendingIntent(spy)
184         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
185         wrapper.onContentUpdated(row)
186         ViewUtils.detachView(root)
187         looper.processAllMessages()
188 
189         val captor = ArgumentCaptor.forClass(CancelListener::class.java)
190         verify(spy, times(1)).registerCancelListener(captor.capture())
191         verify(spy, times(1)).unregisterCancelListener(captor.value)
192     }
193 
194     @Test
actionViewUpdated_oldPendingIntentListenersRemovednull195     fun actionViewUpdated_oldPendingIntentListenersRemoved() {
196         val pi =
197             PendingIntent.getActivity(
198                 mContext,
199                 System.currentTimeMillis().toInt(),
200                 Intent(Intent.ACTION_VIEW),
201                 PendingIntent.FLAG_IMMUTABLE
202             )
203         val spy = Mockito.spy(pi)
204         val action = createActionWithPendingIntent(spy)
205         val wrapper = NotificationTemplateViewWrapper(mContext, view, row)
206         wrapper.onContentUpdated(row)
207         looper.processAllMessages()
208 
209         // Grab set attach listener
210         val attachListener =
211             Mockito.spy(action.getTag(com.android.systemui.res.R.id.pending_intent_listener_tag))
212                 as ActionPendingIntentCancellationHandler
213         action.setTag(com.android.systemui.res.R.id.pending_intent_listener_tag, attachListener)
214 
215         // Update pending intent in the existing action
216         val newPi =
217             PendingIntent.getActivity(
218                 mContext,
219                 System.currentTimeMillis().toInt(),
220                 Intent(Intent.ACTION_ALARM_CHANGED),
221                 PendingIntent.FLAG_IMMUTABLE
222             )
223         action.setTagInternal(R.id.pending_intent_tag, newPi)
224         wrapper.onContentUpdated(row)
225         looper.processAllMessages()
226 
227         // Listeners for original pending intent need to be cleaned up now.
228         val captor = ArgumentCaptor.forClass(CancelListener::class.java)
229         verify(spy, times(1)).registerCancelListener(captor.capture())
230         verify(spy, times(1)).unregisterCancelListener(captor.value)
231         // Attach listener has to be replaced with a new one.
232         assertThat(action.getTag(com.android.systemui.res.R.id.pending_intent_listener_tag))
233             .isNotEqualTo(attachListener)
234         verify(attachListener).remove()
235     }
236 
createActionWithPendingIntentnull237     private fun createActionWithPendingIntent(): View {
238         val pi =
239             PendingIntent.getActivity(
240                 mContext,
241                 System.currentTimeMillis().toInt(),
242                 Intent(Intent.ACTION_VIEW),
243                 PendingIntent.FLAG_IMMUTABLE
244             )
245         return createActionWithPendingIntent(pi)
246     }
247 
createActionWithPendingIntentnull248     private fun createActionWithPendingIntent(pi: PendingIntent): View {
249         val view =
250             LayoutInflater.from(mContext)
251                 .inflate(R.layout.notification_material_action, null, false)
252         view.setTagInternal(R.id.pending_intent_tag, pi)
253         actions.addView(view)
254         return view
255     }
256 
getPendingIntentnull257     private fun getPendingIntent(action: View): PendingIntent {
258         val pendingIntent = action.getTag(R.id.pending_intent_tag) as PendingIntent
259         assertThat(pendingIntent).isNotNull()
260         return pendingIntent
261     }
262 
waitForActionDisablednull263     private fun waitForActionDisabled(action: View) {
264         waitForCondition {
265             looper.processAllMessages()
266             !action.isEnabled
267         }
268     }
269 }
270