Merge "Add dialog for recommended controls" into rvc-dev
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 5d2e303..38e18a9 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -39,6 +39,7 @@
         <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
         <permission name="android.permission.OBSERVE_NETWORK_POLICY"/>
         <permission name="android.permission.OVERRIDE_WIFI_CONFIG"/>
+        <permission name="android.permission.PACKAGE_USAGE_STATS" />
         <permission name="android.permission.READ_DREAM_STATE"/>
         <permission name="android.permission.READ_FRAME_BUFFER"/>
         <permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 617ed4e..30b461d 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -180,6 +180,8 @@
 
     <!-- Adding Controls to SystemUI -->
     <uses-permission android:name="android.permission.BIND_CONTROLS" />
+    <!-- Check foreground controls applications -->
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
 
     <!-- Quick Settings tile: Night Mode / Dark Theme -->
     <uses-permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE" />
@@ -686,6 +688,25 @@
                   android:visibleToInstantApps="true">
         </activity>
 
+        <receiver android:name=".controls.management.ControlsRequestReceiver">
+            <intent-filter>
+                <action android:name="android.service.controls.action.ADD_CONTROL" />
+            </intent-filter>
+        </receiver>
+
+        <!-- started from ControlsFavoritingActivity -->
+        <activity
+            android:name=".controls.management.ControlsRequestDialog"
+            android:exported="true"
+            android:theme="@style/Theme.ControlsRequestDialog"
+            android:finishOnCloseSystemDialogs="true"
+            android:showForAllUsers="true"
+            android:clearTaskOnLaunch="true"
+            android:launchMode="singleTask"
+            android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
+            android:excludeFromRecents="true"
+            android:visibleToInstantApps="true"/>
+
         <!-- Doze with notifications, run in main sysui process for every user  -->
         <service
             android:name=".doze.DozeService"
diff --git a/packages/SystemUI/res/layout/controls_dialog.xml b/packages/SystemUI/res/layout/controls_dialog.xml
new file mode 100644
index 0000000..3effaf5
--- /dev/null
+++ b/packages/SystemUI/res/layout/controls_dialog.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="@dimen/controls_dialog_padding"
+    android:layout_margin="@dimen/controls_dialog_padding"
+    >
+
+    <include
+        android:id="@+id/control"
+        layout="@layout/controls_base_item"
+        android:layout_width="@dimen/controls_dialog_control_width"
+        android:layout_height="@dimen/control_height"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="@dimen/controls_dialog_padding"
+        android:layout_marginBottom="@dimen/controls_dialog_padding"
+    />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 2dc0f5f..7aaf6f9 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1249,6 +1249,10 @@
     <dimen name="controls_app_divider_side_margin">32dp</dimen>
 
     <dimen name="controls_card_margin">2dp</dimen>
+    <item name="control_card_elevation" type="dimen" format="float">15</item>
+
+    <dimen name="controls_dialog_padding">8dp</dimen>
+    <dimen name="controls_dialog_control_width">200dp</dimen>
 
     <!-- Screen Record -->
     <dimen name="screenrecord_dialog_padding">18dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index bcf3a26..5b28479 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2639,4 +2639,11 @@
     <string name="controls_favorite_load_error">The list of all controls could not be loaded.</string>
     <!-- Controls management controls screen header for Other zone [CHAR LIMIT=60] -->
     <string name="controls_favorite_other_zone_header">Other</string>
+
+    <!-- Controls dialog title [CHAR LIMIT=30] -->
+    <string name="controls_dialog_title">Add to Quick Controls</string>
+    <!-- Controls dialog add to favorites [CHAR LIMIT=30] -->
+    <string name="controls_dialog_ok">Add to favorites</string>
+    <!-- Controls dialog message [CHAR LIMIT=NONE] -->
+    <string name="controls_dialog_message"><xliff:g id="app" example="System UI">%s</xliff:g> suggested this control to add to your favorites.</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 2eccf58..125dd8f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -697,4 +697,7 @@
         <!-- used to override dark/light theming -->
         <item name="*android:colorPopupBackground">@color/control_list_popup_background</item>
     </style>
+
+    <style name="Theme.ControlsRequestDialog" parent="@style/Theme.SystemUI.MediaProjectionAlertDialog"/>
+
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
index f2881d4..7eafe2e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt
@@ -124,6 +124,18 @@
     fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo>
 
     /**
+     * Adds a single favorite to a given component and structure
+     * @param componentName the name of the service that provides the [Control]
+     * @param structureName the name of the structure that holds the [Control]
+     * @param controlInfo persistent information about the [Control] to be added.
+     */
+    fun addFavorite(
+        componentName: ComponentName,
+        structureName: CharSequence,
+        controlInfo: ControlInfo
+    )
+
+    /**
      * Replaces the favorites for the given structure.
      *
      * Calling this method will eliminate the previous selection of favorites and replace it with a
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index dedd341..f818d19 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -300,6 +300,19 @@
         bindingController.unsubscribe()
     }
 
+    override fun addFavorite(
+        componentName: ComponentName,
+        structureName: CharSequence,
+        controlInfo: ControlInfo
+    ) {
+        if (!confirmAvailability()) return
+        executor.execute {
+            if (Favorites.addFavorite(componentName, structureName, controlInfo)) {
+                persistenceWrapper.storeFavorites(Favorites.getAllStructures())
+            }
+        }
+    }
+
     override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
         if (!confirmAvailability()) return
         executor.execute {
@@ -437,6 +450,24 @@
         favMap = newFavMap
     }
 
+    fun addFavorite(
+        componentName: ComponentName,
+        structureName: CharSequence,
+        controlInfo: ControlInfo
+    ): Boolean {
+        // Check if control is in favorites
+        if (getControlsForComponent(componentName)
+                        .any { it.controlId == controlInfo.controlId }) {
+            return false
+        }
+        val structureInfo = favMap.get(componentName)
+                ?.firstOrNull { it.structure == structureName }
+                ?: StructureInfo(componentName, structureName, emptyList())
+        val newStructureInfo = structureInfo.copy(controls = structureInfo.controls + controlInfo)
+        replaceControls(newStructureInfo)
+        return true
+    }
+
     fun replaceControls(updatedStructure: StructureInfo) {
         val newFavMap = favMap.toMutableMap()
         val structures = mutableListOf<StructureInfo>()
@@ -456,8 +487,8 @@
             structures.add(updatedStructure)
         }
 
-        newFavMap.put(componentName, structures.toList())
-        favMap = newFavMap.toMap()
+        newFavMap.put(componentName, structures)
+        favMap = newFavMap
     }
 
     fun clear() {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index 859311e..946a236 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.controls.management.ControlsListingControllerImpl
 import com.android.systemui.controls.management.ControlsProviderSelectorActivity
+import com.android.systemui.controls.management.ControlsRequestDialog
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.controls.ui.ControlsUiControllerImpl
 import dagger.Binds
@@ -69,4 +70,11 @@
     abstract fun provideControlsFavoritingActivity(
         activity: ControlsFavoritingActivity
     ): Activity
+
+    @Binds
+    @IntoMap
+    @ClassKey(ControlsRequestDialog::class)
+    abstract fun provideControlsRequestDialog(
+        activity: ControlsRequestDialog
+    ): Activity
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
index e87cf74..c21f724 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt
@@ -42,7 +42,8 @@
  * @param onlyFavorites set to true to only display favorites instead of all controls
  */
 class ControlAdapter(
-    private val layoutInflater: LayoutInflater
+    private val layoutInflater: LayoutInflater,
+    private val elevation: Float
 ) : RecyclerView.Adapter<Holder>() {
 
     companion object {
@@ -66,7 +67,7 @@
                         layoutParams.apply {
                             width = ViewGroup.LayoutParams.MATCH_PARENT
                         }
-                        elevation = 15f
+                        elevation = this@ControlAdapter.elevation
                     }
                 ) { id, favorite ->
                     model?.changeFavoriteStatus(id, favorite)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index 08a1a50..471f9d3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -142,8 +142,9 @@
         val margin = resources.getDimensionPixelSize(R.dimen.controls_card_margin)
         val itemDecorator = MarginItemDecorator(margin, margin)
         val layoutInflater = LayoutInflater.from(applicationContext)
+        val elevation = resources.getFloat(R.dimen.control_card_elevation)
 
-        adapterAll = ControlAdapter(layoutInflater)
+        adapterAll = ControlAdapter(layoutInflater, elevation)
         recyclerViewAll = requireViewById<RecyclerView>(R.id.listAll).apply {
             adapter = adapterAll
             layoutManager = GridLayoutManager(applicationContext, 2).apply {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
new file mode 100644
index 0000000..463632b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2020 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.controls.management
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.UserHandle
+import android.service.controls.Control
+import android.service.controls.ControlsProviderService
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.controller.ControlInfo
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.ui.RenderInfo
+import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.LifecycleActivity
+import javax.inject.Inject
+
+class ControlsRequestDialog @Inject constructor(
+    private val controller: ControlsController,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val controlsListingController: ControlsListingController
+) : LifecycleActivity(), DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+
+    companion object {
+        private const val TAG = "ControlsRequestDialog"
+    }
+
+    private lateinit var component: ComponentName
+    private lateinit var control: Control
+    private var dialog: Dialog? = null
+    private val callback = object : ControlsListingController.ControlsListingCallback {
+        override fun onServicesUpdated(candidates: List<ControlsServiceInfo>) {}
+    }
+
+    private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
+        private val startingUser = controller.currentUserId
+
+        override fun onUserSwitched(newUserId: Int) {
+            if (newUserId != startingUser) {
+                stopTracking()
+                finish()
+            }
+        }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (!controller.available) {
+            Log.w(TAG, "Quick Controls not available for this user ")
+            finish()
+        }
+        currentUserTracker.startTracking()
+        controlsListingController.addCallback(callback)
+
+        val requestUser = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL)
+        val currentUser = controller.currentUserId
+
+        if (requestUser != currentUser) {
+            Log.w(TAG, "Current user ($currentUser) different from request user ($requestUser)")
+            finish()
+        }
+
+        component = intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME) ?: run {
+            Log.e(TAG, "Request did not contain componentName")
+            finish()
+            return
+        }
+
+        control = intent.getParcelableExtra(ControlsProviderService.EXTRA_CONTROL) ?: run {
+            Log.e(TAG, "Request did not contain control")
+            finish()
+            return
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        val label = verifyComponentAndGetLabel()
+        if (label == null) {
+            Log.e(TAG, "The component specified (${component.flattenToString()} " +
+                    "is not a valid ControlsProviderService")
+            finish()
+            return
+        }
+
+        if (isCurrentFavorite()) {
+            Log.w(TAG, "The control ${control.title} is already a favorite")
+            finish()
+        }
+
+        dialog = createDialog(label)
+
+        dialog?.show()
+    }
+
+    override fun onDestroy() {
+        dialog?.dismiss()
+        currentUserTracker.stopTracking()
+        controlsListingController.removeCallback(callback)
+        super.onDestroy()
+    }
+
+    private fun verifyComponentAndGetLabel(): CharSequence? {
+        return controlsListingController.getAppLabel(component)
+    }
+
+    private fun isCurrentFavorite(): Boolean {
+        val favorites = controller.getFavoritesForComponent(component)
+        return favorites.any { it.controls.any { it.controlId == control.controlId } }
+    }
+
+    fun createDialog(label: CharSequence): Dialog {
+
+        val renderInfo = RenderInfo.lookup(control.deviceType, true)
+        val frame = LayoutInflater.from(this).inflate(R.layout.controls_dialog, null).apply {
+            requireViewById<ImageView>(R.id.icon).apply {
+                setImageIcon(Icon.createWithResource(context, renderInfo.iconResourceId))
+                setImageTintList(
+                        context.resources.getColorStateList(renderInfo.foreground, context.theme))
+            }
+            requireViewById<TextView>(R.id.title).text = control.title
+            requireViewById<TextView>(R.id.subtitle).text = control.subtitle
+            requireViewById<View>(R.id.control).elevation =
+                    resources.getFloat(R.dimen.control_card_elevation)
+        }
+
+        val dialog = AlertDialog.Builder(this)
+                .setTitle(getString(R.string.controls_dialog_title))
+                .setMessage(getString(R.string.controls_dialog_message, label))
+                .setPositiveButton(R.string.controls_dialog_ok, this)
+                .setNegativeButton(android.R.string.cancel, this)
+                .setOnCancelListener(this)
+                .setView(frame)
+                .create()
+
+        SystemUIDialog.registerDismissListener(dialog)
+        dialog.setCanceledOnTouchOutside(true)
+        return dialog
+    }
+
+    override fun onCancel(dialog: DialogInterface?) {
+        finish()
+    }
+
+    override fun onClick(dialog: DialogInterface?, which: Int) {
+        if (which == Dialog.BUTTON_POSITIVE) {
+            controller.addFavorite(componentName, control.structure ?: "",
+                    ControlInfo(control.controlId, control.title, control.deviceType))
+        }
+        finish()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt
new file mode 100644
index 0000000..5c30b5a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 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.controls.management
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.service.controls.Control
+import android.service.controls.ControlsProviderService
+import android.util.Log
+
+/**
+ * Proxy to launch in user 0
+ */
+class ControlsRequestReceiver : BroadcastReceiver() {
+
+    companion object {
+        private const val TAG = "ControlsRequestReceiver"
+
+        fun isPackageInForeground(context: Context, packageName: String): Boolean {
+            val uid = try {
+                context.packageManager.getPackageUid(packageName, 0)
+            } catch (_: PackageManager.NameNotFoundException) {
+                Log.w(TAG, "Package $packageName not found")
+                return false
+            }
+
+            val am = context.getSystemService(ActivityManager::class.java)
+            if ((am?.getUidImportance(uid) ?: IMPORTANCE_GONE) != IMPORTANCE_FOREGROUND) {
+                Log.w(TAG, "Uid $uid not in foreground")
+                return false
+            }
+            return true
+        }
+    }
+
+    override fun onReceive(context: Context, intent: Intent) {
+
+        val packageName = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME)
+                ?.packageName
+
+        if (packageName == null || !isPackageInForeground(context, packageName)) {
+            return
+        }
+
+        val activityIntent = Intent(context, ControlsRequestDialog::class.java).apply {
+            Intent.EXTRA_COMPONENT_NAME.let {
+                putExtra(it, intent.getParcelableExtra<ComponentName>(it))
+            }
+            ControlsProviderService.EXTRA_CONTROL.let {
+                putExtra(it, intent.getParcelableExtra<Control>(it))
+            }
+        }
+        activityIntent.putExtra(Intent.EXTRA_USER_ID, context.userId)
+
+        context.startActivityAsUser(activityIntent, UserHandle.SYSTEM)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt
new file mode 100644
index 0000000..663f011
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2020 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.controls.management
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
+import android.content.ComponentName
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.service.controls.Control
+import android.service.controls.ControlsProviderService
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ControlsRequestReceiverTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var packageManager: PackageManager
+    @Mock
+    private lateinit var activityManager: ActivityManager
+    @Mock
+    private lateinit var control: Control
+
+    private val componentName = ComponentName("test_pkg", "test_cls")
+    private lateinit var receiver: ControlsRequestReceiver
+    private lateinit var wrapper: MyWrapper
+    private lateinit var intent: Intent
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        mContext.setMockPackageManager(packageManager)
+        mContext.addMockSystemService(ActivityManager::class.java, activityManager)
+
+        receiver = ControlsRequestReceiver()
+
+        wrapper = MyWrapper(context)
+
+        intent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply {
+            putExtra(Intent.EXTRA_COMPONENT_NAME, componentName)
+            putExtra(ControlsProviderService.EXTRA_CONTROL, control)
+        }
+    }
+
+    @Test
+    fun testPackageVerification_nonExistentPackage() {
+        `when`(packageManager.getPackageUid(anyString(), anyInt()))
+                .thenThrow(PackageManager.NameNotFoundException::class.java)
+
+        assertFalse(ControlsRequestReceiver.isPackageInForeground(mContext, "TEST"))
+    }
+
+    @Test
+    fun testPackageVerification_uidNotInForeground() {
+        `when`(packageManager.getPackageUid(anyString(), anyInt())).thenReturn(12345)
+
+        `when`(activityManager.getUidImportance(anyInt())).thenReturn(IMPORTANCE_GONE)
+
+        assertFalse(ControlsRequestReceiver.isPackageInForeground(mContext, "TEST"))
+    }
+
+    @Test
+    fun testPackageVerification_OK() {
+        `when`(packageManager.getPackageUid(anyString(), anyInt())).thenReturn(12345)
+
+        `when`(activityManager.getUidImportance(anyInt())).thenReturn(IMPORTANCE_GONE)
+        `when`(activityManager.getUidImportance(12345)).thenReturn(IMPORTANCE_FOREGROUND)
+
+        assertTrue(ControlsRequestReceiver.isPackageInForeground(mContext, "TEST"))
+    }
+
+    @Test
+    fun testOnReceive_packageNotVerified_nameNotFound() {
+        `when`(packageManager.getPackageUid(eq(componentName.packageName), anyInt()))
+                .thenThrow(PackageManager.NameNotFoundException::class.java)
+
+        receiver.onReceive(wrapper, intent)
+
+        assertNull(wrapper.intent)
+    }
+
+    @Test
+    fun testOnReceive_packageNotVerified_notForeground() {
+        `when`(packageManager.getPackageUid(eq(componentName.packageName), anyInt()))
+                .thenReturn(12345)
+
+        `when`(activityManager.getUidImportance(anyInt())).thenReturn(IMPORTANCE_GONE)
+
+        receiver.onReceive(wrapper, intent)
+
+        assertNull(wrapper.intent)
+    }
+
+    @Test
+    fun testOnReceive_OK() {
+        `when`(packageManager.getPackageUid(eq(componentName.packageName), anyInt()))
+                .thenReturn(12345)
+
+        `when`(activityManager.getUidImportance(eq(12345))).thenReturn(IMPORTANCE_FOREGROUND)
+
+        receiver.onReceive(wrapper, intent)
+
+        wrapper.intent?.let {
+            assertEquals(ComponentName(wrapper, ControlsRequestDialog::class.java), it.component)
+
+            assertEquals(control, it.getParcelableExtra(ControlsProviderService.EXTRA_CONTROL))
+
+            assertEquals(componentName, it.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME))
+        } ?: run { fail("Null start intent") }
+    }
+
+    class MyWrapper(context: Context) : ContextWrapper(context) {
+        var intent: Intent? = null
+
+        override fun startActivityAsUser(intent: Intent, user: UserHandle) {
+            // Always launch activity as system
+            assertTrue(user == UserHandle.SYSTEM)
+            this.intent = intent
+        }
+
+        override fun startActivity(intent: Intent) {
+            this.intent = intent
+        }
+    }
+}
\ No newline at end of file