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