diff options
| author | 2019-08-14 20:11:32 +0000 | |
|---|---|---|
| committer | 2019-08-14 20:11:32 +0000 | |
| commit | 2fa208d98abe6cde55dc34142e016a3358df49e9 (patch) | |
| tree | d421e1fca1d1eae5f65b6a059c958db61f747309 | |
| parent | 07820f85ace04cfb935c214b17e99db8620f515f (diff) | |
| parent | 42f86a08f156ad167c36c223ae45ff2d2ce4a54b (diff) | |
Merge "Universal Broadcast Receiver for SystemUI"
5 files changed, 711 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 0ee9bff50bf3..59270a06dfbc 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -34,6 +34,7 @@ import com.android.keyguard.clock.ClockManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.appops.AppOpsController; import com.android.systemui.assist.AssistManager; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dock.DockManager; @@ -200,6 +201,7 @@ public class Dependency extends SystemUI { @Inject Lazy<ActivityStarter> mActivityStarter; @Inject Lazy<ActivityStarterDelegate> mActivityStarterDelegate; + @Inject Lazy<BroadcastDispatcher> mBroadcastDispatcher; @Inject Lazy<AsyncSensorManager> mAsyncSensorManager; @Inject Lazy<BluetoothController> mBluetoothController; @Inject Lazy<LocationController> mLocationController; @@ -317,6 +319,7 @@ public class Dependency extends SystemUI { mProviders.put(MAIN_HANDLER, mMainHandler::get); mProviders.put(ActivityStarter.class, mActivityStarter::get); mProviders.put(ActivityStarterDelegate.class, mActivityStarterDelegate::get); + mProviders.put(BroadcastDispatcher.class, mBroadcastDispatcher::get); mProviders.put(AsyncSensorManager.class, mAsyncSensorManager::get); @@ -496,6 +499,7 @@ public class Dependency extends SystemUI { // Make sure that the DumpController gets added to mDependencies, as they are only added // with Dependency#get. getDependency(DumpController.class); + getDependency(BroadcastDispatcher.class); // If an arg is specified, try to dump the dependency String controller = args != null && args.length > 1 diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt new file mode 100644 index 000000000000..f0e8c16e650a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2019 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.BroadcastReceiver +import android.content.Context +import android.content.IntentFilter +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.UserHandle +import android.util.Log +import android.util.SparseArray +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.Dependency.BG_LOOPER_NAME +import com.android.systemui.Dependency.MAIN_HANDLER_NAME +import com.android.systemui.Dumpable +import java.io.FileDescriptor +import java.io.PrintWriter +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +data class ReceiverData( + val receiver: BroadcastReceiver, + val filter: IntentFilter, + val handler: Handler, + val user: UserHandle +) + +private const val MSG_ADD_RECEIVER = 0 +private const val MSG_REMOVE_RECEIVER = 1 +private const val MSG_REMOVE_RECEIVER_FOR_USER = 2 +private const val TAG = "BroadcastDispatcher" +private const val DEBUG = false + +/** + * SystemUI master Broadcast Dispatcher. + * + * This class allows [BroadcastReceiver] to register and centralizes registrations to [Context] + * from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for + * a given broadcast. + * + * Use only for IntentFilters with actions and optionally categories. It does not support, + * permissions, schemes or data types. Cannot be used for getting sticky broadcasts. + */ +@Singleton +open class BroadcastDispatcher @Inject constructor ( + private val context: Context, + @Named(MAIN_HANDLER_NAME) private val mainHandler: Handler, + @Named(BG_LOOPER_NAME) private val bgLooper: Looper +) : Dumpable { + + // Only modify in BG thread + private val receiversByUser = SparseArray<UserBroadcastDispatcher>(20) + + /** + * Register a receiver for broadcast with the dispatcher + * + * @param receiver A receiver to dispatch the [Intent] + * @param filter A filter to determine what broadcasts should be dispatched to this receiver. + * It will only take into account actions and categories for filtering. + * @param handler A handler to dispatch [BroadcastReceiver.onReceive]. By default, it is the + * main handler. + * @param user A user handle to determine which broadcast should be dispatched to this receiver. + * By default, it is the current user. + */ + @JvmOverloads + fun registerReceiver( + receiver: BroadcastReceiver, + filter: IntentFilter, + handler: Handler = mainHandler, + user: UserHandle = context.user + ) { + this.handler.obtainMessage(MSG_ADD_RECEIVER, ReceiverData(receiver, filter, handler, user)) + .sendToTarget() + } + + /** + * Unregister receiver for all users. + * <br> + * This will remove every registration of [receiver], not those done just with [UserHandle.ALL]. + * + * @param receiver The receiver to unregister. It will be unregistered for all users. + */ + fun unregisterReceiver(receiver: BroadcastReceiver) { + handler.obtainMessage(MSG_REMOVE_RECEIVER, receiver).sendToTarget() + } + + /** + * Unregister receiver for a particular user. + * + * @param receiver The receiver to unregister. It will be unregistered for all users. + * @param user The user associated to the registered [receiver]. It can be [UserHandle.ALL]. + */ + fun unregisterReceiverForUser(receiver: BroadcastReceiver, user: UserHandle) { + handler.obtainMessage(MSG_REMOVE_RECEIVER_FOR_USER, user.identifier, 0, receiver) + .sendToTarget() + } + + @VisibleForTesting + protected open fun createUBRForUser(userId: Int) = + UserBroadcastDispatcher(context, userId, mainHandler, bgLooper) + + override fun dump(fd: FileDescriptor?, pw: PrintWriter?, args: Array<out String>?) { + pw?.println("Broadcast dispatcher:") + for (index in 0 until receiversByUser.size()) { + pw?.println(" User ${receiversByUser.keyAt(index)}") + receiversByUser.valueAt(index).dump(fd, pw, args) + } + } + + private val handler = object : Handler(bgLooper) { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_ADD_RECEIVER -> { + val data = msg.obj as ReceiverData + val userId = data.user.identifier + if (userId < UserHandle.USER_ALL) { + if (DEBUG) Log.w(TAG, "Register receiver for invalid user: $userId") + return + } + val uBR = receiversByUser.get(userId, createUBRForUser(userId)) + receiversByUser.put(userId, uBR) + uBR.registerReceiver(data) + } + + MSG_REMOVE_RECEIVER -> { + for (it in 0 until receiversByUser.size()) { + receiversByUser.valueAt(it).unregisterReceiver(msg.obj as BroadcastReceiver) + } + } + + MSG_REMOVE_RECEIVER_FOR_USER -> { + receiversByUser.get(msg.arg1)?.unregisterReceiver(msg.obj as BroadcastReceiver) + } + + else -> super.handleMessage(msg) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt new file mode 100644 index 000000000000..d44b63e813e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2019 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.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.UserHandle +import android.util.ArrayMap +import android.util.ArraySet +import android.util.Log +import com.android.systemui.Dumpable +import java.io.FileDescriptor +import java.io.PrintWriter +import java.util.concurrent.atomic.AtomicBoolean + +private const val MSG_REGISTER_RECEIVER = 0 +private const val MSG_UNREGISTER_RECEIVER = 1 +private const val TAG = "UniversalReceiver" +private const val DEBUG = false + +/** + * Broadcast dispatcher for a given user registration [userId]. + * + * Created by [BroadcastDispatcher] as needed by users. The value of [userId] can be + * [UserHandle.USER_ALL]. + */ +class UserBroadcastDispatcher( + private val context: Context, + private val userId: Int, + private val mainHandler: Handler, + private val bgLooper: Looper +) : BroadcastReceiver(), Dumpable { + + private val bgHandler = object : Handler(bgLooper) { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_REGISTER_RECEIVER -> handleRegisterReceiver(msg.obj as ReceiverData) + MSG_UNREGISTER_RECEIVER -> handleUnregisterReceiver(msg.obj as BroadcastReceiver) + else -> Unit + } + } + } + + private val registered = AtomicBoolean(false) + + internal fun isRegistered() = registered.get() + + private val registerReceiver = Runnable { + val categories = mutableSetOf<String>() + receiverToReceiverData.values.flatten().forEach { + it.filter.categoriesIterator()?.asSequence()?.let { + categories.addAll(it) + } + } + val intentFilter = IntentFilter().apply { + actionsToReceivers.keys.forEach { addAction(it) } + categories.forEach { addCategory(it) } + } + + if (registered.get()) { + context.unregisterReceiver(this) + registered.set(false) + } + // Short interval without receiver, this can be problematic + if (intentFilter.countActions() > 0 && !registered.get()) { + context.registerReceiverAsUser( + this, + UserHandle.of(userId), + intentFilter, + null, + bgHandler) + registered.set(true) + } + } + + // Only modify in BG thread + private val actionsToReceivers = ArrayMap<String, MutableSet<ReceiverData>>() + private val receiverToReceiverData = ArrayMap<BroadcastReceiver, MutableSet<ReceiverData>>() + + override fun onReceive(context: Context, intent: Intent) { + bgHandler.post(HandleBroadcastRunnable(actionsToReceivers, context, intent)) + } + + /** + * Register a [ReceiverData] for this user. + */ + fun registerReceiver(receiverData: ReceiverData) { + bgHandler.obtainMessage(MSG_REGISTER_RECEIVER, receiverData).sendToTarget() + } + + /** + * Unregister a given [BroadcastReceiver] for this user. + */ + fun unregisterReceiver(receiver: BroadcastReceiver) { + bgHandler.obtainMessage(MSG_UNREGISTER_RECEIVER, receiver).sendToTarget() + } + + private fun handleRegisterReceiver(receiverData: ReceiverData) { + if (DEBUG) Log.w(TAG, "Register receiver: ${receiverData.receiver}") + receiverToReceiverData.getOrPut(receiverData.receiver, { ArraySet() }).add(receiverData) + var changed = false + // Index the BroadcastReceiver by all its actions, that way it's easier to dispatch given + // a received intent. + receiverData.filter.actionsIterator().forEach { + actionsToReceivers.getOrPut(it) { + changed = true + ArraySet() + }.add(receiverData) + } + if (changed) { + mainHandler.post(registerReceiver) + } + } + + private fun handleUnregisterReceiver(receiver: BroadcastReceiver) { + if (DEBUG) Log.w(TAG, "Unregister receiver: $receiver") + val actions = receiverToReceiverData.getOrElse(receiver) { return } + .flatMap { it.filter.actionsIterator().asSequence().asIterable() }.toSet() + receiverToReceiverData.get(receiver)?.clear() + var changed = false + actions.forEach { action -> + actionsToReceivers.get(action)?.removeIf { it.receiver == receiver } + if (actionsToReceivers.get(action)?.isEmpty() ?: false) { + changed = true + actionsToReceivers.remove(action) + } + } + if (changed) { + mainHandler.post(registerReceiver) + } + } + + override fun dump(fd: FileDescriptor?, pw: PrintWriter?, args: Array<out String>?) { + pw?.println(" Registered=${registered.get()}") + actionsToReceivers.forEach { (action, list) -> + pw?.println(" $action:") + list.forEach { pw?.println(" ${it.receiver}") } + } + } + + private class HandleBroadcastRunnable( + val actionsToReceivers: Map<String, Set<ReceiverData>>, + val context: Context, + val intent: Intent + ) : Runnable { + override fun run() { + if (DEBUG) Log.w(TAG, "Dispatching $intent") + actionsToReceivers.get(intent.action) + ?.filter { + it.filter.hasAction(intent.action) && + it.filter.matchCategories(intent.categories) == null } + ?.forEach { + it.handler.post { + if (DEBUG) Log.w(TAG, "Dispatching to ${it.receiver}") + it.receiver.onReceive(context, intent) + } + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt new file mode 100644 index 000000000000..2bff5488937d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2019 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.BroadcastReceiver +import android.content.Context +import android.content.IntentFilter +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import com.android.systemui.SysuiTestCase +import junit.framework.Assert.assertSame +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@SmallTest +class BroadcastDispatcherTest : SysuiTestCase() { + + companion object { + val user0 = UserHandle.of(0) + val user1 = UserHandle.of(1) + + fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + } + + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockUBRUser0: UserBroadcastDispatcher + @Mock + private lateinit var mockUBRUser1: UserBroadcastDispatcher + @Mock + private lateinit var broadcastReceiver: BroadcastReceiver + @Mock + private lateinit var broadcastReceiverOther: BroadcastReceiver + @Mock + private lateinit var intentFilter: IntentFilter + @Mock + private lateinit var intentFilterOther: IntentFilter + @Mock + private lateinit var mockHandler: Handler + + @Captor + private lateinit var argumentCaptor: ArgumentCaptor<ReceiverData> + + private lateinit var testableLooper: TestableLooper + private lateinit var broadcastDispatcher: BroadcastDispatcher + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + + broadcastDispatcher = TestBroadcastDispatcher( + mockContext, + Handler(testableLooper.looper), + testableLooper.looper, + mapOf(0 to mockUBRUser0, 1 to mockUBRUser1)) + } + + @Test + fun testAddingReceiverToCorrectUBR() { + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mockHandler, user0) + broadcastDispatcher.registerReceiver( + broadcastReceiverOther, intentFilterOther, mockHandler, user1) + + testableLooper.processAllMessages() + + verify(mockUBRUser0).registerReceiver(capture(argumentCaptor)) + + assertSame(broadcastReceiver, argumentCaptor.value.receiver) + assertSame(intentFilter, argumentCaptor.value.filter) + + verify(mockUBRUser1).registerReceiver(capture(argumentCaptor)) + assertSame(broadcastReceiverOther, argumentCaptor.value.receiver) + assertSame(intentFilterOther, argumentCaptor.value.filter) + } + + @Test + fun testRemovingReceiversRemovesFromAllUBR() { + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mockHandler, user0) + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mockHandler, user1) + + broadcastDispatcher.unregisterReceiver(broadcastReceiver) + + testableLooper.processAllMessages() + + verify(mockUBRUser0).unregisterReceiver(broadcastReceiver) + verify(mockUBRUser1).unregisterReceiver(broadcastReceiver) + } + + @Test + fun testRemoveReceiverFromUser() { + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mockHandler, user0) + broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mockHandler, user1) + + broadcastDispatcher.unregisterReceiverForUser(broadcastReceiver, user0) + + testableLooper.processAllMessages() + + verify(mockUBRUser0).unregisterReceiver(broadcastReceiver) + verify(mockUBRUser1, never()).unregisterReceiver(broadcastReceiver) + } + + private class TestBroadcastDispatcher( + context: Context, + mainHandler: Handler, + bgLooper: Looper, + var mockUBRMap: Map<Int, UserBroadcastDispatcher> + ) : BroadcastDispatcher(context, mainHandler, bgLooper) { + override fun createUBRForUser(userId: Int): UserBroadcastDispatcher { + return mockUBRMap.getOrDefault(userId, mock(UserBroadcastDispatcher::class.java)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt new file mode 100644 index 000000000000..011c2cd57588 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/UserBroadcastDispatcherTest.kt @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2019 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.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.os.UserHandle +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import com.android.systemui.SysuiTestCase +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.anyString +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@SmallTest +class UserBroadcastDispatcherTest : SysuiTestCase() { + + companion object { + private const val ACTION_1 = "com.android.systemui.tests.ACTION_1" + private const val ACTION_2 = "com.android.systemui.tests.ACTION_2" + private const val CATEGORY_1 = "com.android.systemui.tests.CATEGORY_1" + private const val CATEGORY_2 = "com.android.systemui.tests.CATEGORY_2" + private const val USER_ID = 0 + private val USER_HANDLE = UserHandle.of(USER_ID) + + fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + } + + @Mock + private lateinit var broadcastReceiver: BroadcastReceiver + @Mock + private lateinit var broadcastReceiverOther: BroadcastReceiver + @Mock + private lateinit var mockContext: Context + @Mock + private lateinit var mockHandler: Handler + + @Captor + private lateinit var argumentCaptor: ArgumentCaptor<IntentFilter> + + private lateinit var testableLooper: TestableLooper + private lateinit var universalBroadcastReceiver: UserBroadcastDispatcher + private lateinit var intentFilter: IntentFilter + private lateinit var intentFilterOther: IntentFilter + private lateinit var handler: Handler + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + handler = Handler(testableLooper.looper) + + universalBroadcastReceiver = UserBroadcastDispatcher( + mockContext, USER_ID, handler, testableLooper.looper) + } + + @Test + fun testNotRegisteredOnStart() { + testableLooper.processAllMessages() + verify(mockContext, never()).registerReceiver(any(), any()) + verify(mockContext, never()).registerReceiver(any(), any(), anyInt()) + verify(mockContext, never()).registerReceiver(any(), any(), anyString(), any()) + verify(mockContext, never()).registerReceiver(any(), any(), anyString(), any(), anyInt()) + verify(mockContext, never()).registerReceiverAsUser(any(), any(), any(), anyString(), any()) + } + + @Test + fun testSingleReceiverRegistered() { + intentFilter = IntentFilter(ACTION_1) + + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilter, mockHandler, USER_HANDLE)) + testableLooper.processAllMessages() + + assertTrue(universalBroadcastReceiver.isRegistered()) + verify(mockContext).registerReceiverAsUser( + any(), + eq(USER_HANDLE), + capture(argumentCaptor), + any(), + any()) + assertEquals(1, argumentCaptor.value.countActions()) + assertTrue(argumentCaptor.value.hasAction(ACTION_1)) + assertEquals(0, argumentCaptor.value.countCategories()) + } + + @Test + fun testSingleReceiverUnregistered() { + intentFilter = IntentFilter(ACTION_1) + + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilter, mockHandler, USER_HANDLE)) + testableLooper.processAllMessages() + reset(mockContext) + + assertTrue(universalBroadcastReceiver.isRegistered()) + + universalBroadcastReceiver.unregisterReceiver(broadcastReceiver) + testableLooper.processAllMessages() + + verify(mockContext, atLeastOnce()).unregisterReceiver(any()) + verify(mockContext, never()).registerReceiverAsUser(any(), any(), any(), any(), any()) + assertFalse(universalBroadcastReceiver.isRegistered()) + } + + @Test + fun testFilterHasAllActionsAndCategories_twoReceivers() { + intentFilter = IntentFilter(ACTION_1) + intentFilterOther = IntentFilter(ACTION_2).apply { + addCategory(CATEGORY_1) + addCategory(CATEGORY_2) + } + + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilter, mockHandler, USER_HANDLE)) + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiverOther, intentFilterOther, mockHandler, USER_HANDLE)) + + testableLooper.processAllMessages() + assertTrue(universalBroadcastReceiver.isRegistered()) + + verify(mockContext, times(2)).registerReceiverAsUser( + any(), + eq(USER_HANDLE), + capture(argumentCaptor), + any(), + any()) + + val lastFilter = argumentCaptor.value + + assertTrue(lastFilter.hasAction(ACTION_1)) + assertTrue(lastFilter.hasAction(ACTION_2)) + assertTrue(lastFilter.hasCategory(CATEGORY_1)) + assertTrue(lastFilter.hasCategory(CATEGORY_1)) + } + + @Test + fun testDispatchToCorrectReceiver() { + intentFilter = IntentFilter(ACTION_1) + intentFilterOther = IntentFilter(ACTION_2) + + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilter, handler, USER_HANDLE)) + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiverOther, intentFilterOther, handler, USER_HANDLE)) + + val intent = Intent(ACTION_2) + + universalBroadcastReceiver.onReceive(mockContext, intent) + testableLooper.processAllMessages() + + verify(broadcastReceiver, never()).onReceive(any(), any()) + verify(broadcastReceiverOther).onReceive(mockContext, intent) + } + + @Test + fun testDispatchToCorrectReceiver_differentFiltersSameReceiver() { + intentFilter = IntentFilter(ACTION_1) + intentFilterOther = IntentFilter(ACTION_2) + + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilter, handler, USER_HANDLE)) + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilterOther, handler, USER_HANDLE)) + + val intent = Intent(ACTION_2) + + universalBroadcastReceiver.onReceive(mockContext, intent) + testableLooper.processAllMessages() + + verify(broadcastReceiver).onReceive(mockContext, intent) + } + + @Test + fun testDispatchIntentWithoutCategories() { + intentFilter = IntentFilter(ACTION_1) + intentFilter.addCategory(CATEGORY_1) + intentFilterOther = IntentFilter(ACTION_1) + intentFilterOther.addCategory(CATEGORY_2) + + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiver, intentFilter, handler, USER_HANDLE)) + universalBroadcastReceiver.registerReceiver( + ReceiverData(broadcastReceiverOther, intentFilterOther, handler, USER_HANDLE)) + + val intent = Intent(ACTION_1) + + universalBroadcastReceiver.onReceive(mockContext, intent) + testableLooper.processAllMessages() + + verify(broadcastReceiver).onReceive(mockContext, intent) + verify(broadcastReceiverOther).onReceive(mockContext, intent) + } +} |