Remove main thread sendBroadcast calls
sendBroadcast ends up with a synchronous Binder call which can block main thread. This moves multiple broadcast calls to background thread to avoid jank.
Bug: 234306007
Bug: 221339831
Test: Unit tests
Change-Id: Ibfb7ea6302a0723c5c01cae3afe4e66395cd1f50
Merged-In: Ibe76a70e08c9bfefc7ff23806089957f17514c31
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastSender.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastSender.kt
new file mode 100644
index 0000000..6615f6b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastSender.kt
@@ -0,0 +1,132 @@
+package com.android.systemui.broadcast
+
+import android.annotation.AnyThread
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.UserHandle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.wakelock.WakeLock
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * SystemUI master Broadcast sender
+ *
+ * This class dispatches broadcasts on background thread to avoid synchronous call to binder. Use
+ * this class instead of calling [Context.sendBroadcast] directly.
+ */
+@SysUISingleton
+class BroadcastSender @Inject constructor(
+ private val context: Context,
+ private val wakeLockBuilder: WakeLock.Builder,
+ @Background private val bgExecutor: Executor
+) {
+
+ private val WAKE_LOCK_TAG = "SysUI:BroadcastSender"
+ private val WAKE_LOCK_SEND_REASON = "sendInBackground"
+
+ /**
+ * Sends broadcast via [Context.sendBroadcast] on background thread to avoid blocking
+ * synchronous binder call.
+ */
+ @AnyThread
+ fun sendBroadcast(intent: Intent) {
+ sendInBackground {
+ context.sendBroadcast(intent)
+ }
+ }
+
+ /**
+ * Sends broadcast via [Context.sendBroadcast] on background thread to avoid blocking
+ * synchronous binder call.
+ */
+ @AnyThread
+ fun sendBroadcast(intent: Intent, receiverPermission: String?) {
+ sendInBackground {
+ context.sendBroadcast(intent, receiverPermission)
+ }
+ }
+
+ /**
+ * Sends broadcast via [Context.sendBroadcastAsUser] on background thread to avoid blocking
+ * synchronous binder call.
+ */
+ @AnyThread
+ fun sendBroadcastAsUser(intent: Intent, userHandle: UserHandle) {
+ sendInBackground {
+ context.sendBroadcastAsUser(intent, userHandle)
+ }
+ }
+
+ /**
+ * Sends broadcast via [Context.sendBroadcastAsUser] on background thread to avoid blocking
+ * synchronous binder call.
+ */
+ @AnyThread
+ fun sendBroadcastAsUser(intent: Intent, userHandle: UserHandle, receiverPermission: String?) {
+ sendInBackground {
+ context.sendBroadcastAsUser(intent, userHandle, receiverPermission)
+ }
+ }
+
+ /**
+ * Sends broadcast via [Context.sendBroadcastAsUser] on background thread to avoid blocking
+ * synchronous binder call.
+ */
+ @AnyThread
+ fun sendBroadcastAsUser(
+ intent: Intent,
+ userHandle: UserHandle,
+ receiverPermission: String?,
+ options: Bundle?
+ ) {
+ sendInBackground {
+ context.sendBroadcastAsUser(intent, userHandle, receiverPermission, options)
+ }
+ }
+
+ /**
+ * Sends broadcast via [Context.sendBroadcastAsUser] on background thread to avoid blocking
+ * synchronous binder call.
+ */
+ @AnyThread
+ fun sendBroadcastAsUser(
+ intent: Intent,
+ userHandle: UserHandle,
+ receiverPermission: String?,
+ appOp: Int
+ ) {
+ sendInBackground {
+ context.sendBroadcastAsUser(intent, userHandle, receiverPermission, appOp)
+ }
+ }
+
+ /**
+ * Sends [Intent.ACTION_CLOSE_SYSTEM_DIALOGS] broadcast to the system.
+ */
+ @AnyThread
+ fun closeSystemDialogs() {
+ sendInBackground {
+ context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+ }
+ }
+
+ /**
+ * Dispatches parameter on background executor while holding a wakelock.
+ */
+ private fun sendInBackground(callable: () -> Unit) {
+ val broadcastWakelock = wakeLockBuilder.setTag(WAKE_LOCK_TAG)
+ .setMaxTimeout(5000)
+ .build()
+ broadcastWakelock.acquire(WAKE_LOCK_SEND_REASON)
+ bgExecutor.execute {
+ try {
+ callable.invoke()
+ } finally {
+ broadcastWakelock.release(WAKE_LOCK_SEND_REASON)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt
new file mode 100644
index 0000000..fbd2c91
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 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.systemui.broadcast
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.wakelock.WakeLockFake
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class BroadcastSenderTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var mockContext: Context
+
+ private lateinit var broadcastSender: BroadcastSender
+ private lateinit var executor: FakeExecutor
+ private lateinit var wakeLock: WakeLockFake
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ executor = FakeExecutor(FakeSystemClock())
+ wakeLock = WakeLockFake()
+ val wakeLockBuilder = WakeLockFake.Builder(mContext)
+ wakeLockBuilder.setWakeLock(wakeLock)
+ broadcastSender = BroadcastSender(mockContext, wakeLockBuilder, executor)
+ }
+
+ @Test
+ fun sendBroadcast_dispatchesWithWakelock() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ broadcastSender.sendBroadcast(intent)
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcast(intent)
+ }
+ }
+
+ @Test
+ fun sendBroadcastWithPermission_dispatchesWithWakelock() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val permission = "Permission"
+ broadcastSender.sendBroadcast(intent, permission)
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcast(intent, permission)
+ }
+ }
+
+ @Test
+ fun sendBroadcastAsUser_dispatchesWithWakelock() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ broadcastSender.sendBroadcastAsUser(intent, UserHandle.ALL)
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcastAsUser(intent, UserHandle.ALL)
+ }
+ }
+
+ @Test
+ fun sendBroadcastAsUserWithPermission_dispatchesWithWakelock() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val permission = "Permission"
+ broadcastSender.sendBroadcastAsUser(intent, UserHandle.ALL, permission)
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcastAsUser(intent, UserHandle.ALL, permission)
+ }
+ }
+
+ @Test
+ fun sendBroadcastAsUserWithPermissionAndOptions_dispatchesWithWakelock() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val permission = "Permission"
+ val options = Bundle()
+ options.putString("key", "value")
+
+ broadcastSender.sendBroadcastAsUser(intent, UserHandle.ALL, permission, options)
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcastAsUser(intent, UserHandle.ALL, permission, options)
+ }
+ }
+
+ @Test
+ fun sendBroadcastAsUserWithPermissionAndAppOp_dispatchesWithWakelock() {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val permission = "Permission"
+
+ broadcastSender.sendBroadcastAsUser(intent, UserHandle.ALL, permission, 12)
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcastAsUser(intent, UserHandle.ALL, permission, 12)
+ }
+ }
+
+ @Test
+ fun sendCloseSystemDialogs_dispatchesWithWakelock() {
+ val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+
+ broadcastSender.closeSystemDialogs()
+
+ runExecutorAssertingWakelock {
+ verify(mockContext).sendBroadcast(intentCaptor.capture())
+ assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
+ }
+ }
+
+ private fun runExecutorAssertingWakelock(verification: () -> Unit) {
+ assertThat(wakeLock.isHeld).isTrue()
+ executor.runAllReady()
+ verification.invoke()
+ assertThat(wakeLock.isHeld).isFalse()
+ }
+}
\ No newline at end of file