Universal Broadcast Receiver for SystemUI

Implements a UBR for SystemUI. All classes subscribing to Context should
subscribe here instead to reduce number of IPC into SystemUI.

Test: atest
Bug: 134566046

Change-Id: I6be24f62bd9b9b3a49a4efdf712e2e73c0d8d0ac
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 0ee9bff..59270a0 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.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 @@
 
     @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 @@
         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 @@
         // 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 0000000..f0e8c16
--- /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 0000000..d44b63e
--- /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 0000000..2bff548
--- /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 0000000..011c2cd
--- /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)
+    }
+}