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