Add dialog and controller for TileServiceRequest
This adds the SystemUI side. As there's no API, it's triggered using
CommandRegistry and outputs response to logcat.
The design of the dialog is not final, so measures and styles will be
changed in a future CL.
Test: atest TileRequestDialogTest TileServiceRequestControllerTest
Test: manual
Fixes: 196056024
Change-Id: I5a52b7cd9db6a7fc79a8751e7702be8c005b8def
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
index 2213d1c..9a9683d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
@@ -60,6 +60,10 @@
public abstract int getDetailY();
+ public View getLabel() {
+ return null;
+ }
+
public View getLabelContainer() {
return null;
}
diff --git a/packages/SystemUI/res/layout/tile_service_request_dialog.xml b/packages/SystemUI/res/layout/tile_service_request_dialog.xml
new file mode 100644
index 0000000..b431d44
--- /dev/null
+++ b/packages/SystemUI/res/layout/tile_service_request_dialog.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_horizontal"
+>
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:textDirection="locale"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog"
+ android:lineHeight="20sp"
+ />
+</LinearLayout>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7d9da43..45014f8 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1636,4 +1636,8 @@
<item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
<dimen name="drag_and_drop_icon_size">70dp</dimen>
+
+ <dimen name="qs_tile_service_request_dialog_width">304dp</dimen>
+ <dimen name="qs_tile_service_request_tile_width">192dp</dimen>
+ <dimen name="qs_tile_service_request_content_space">24dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b9002c3..5e84946 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3022,4 +3022,12 @@
<string name="wifi_failed_connect_message">Failed to connect to network</string>
<!-- Provider Model: Title to see all the networks [CHAR LIMIT=50] -->
<string name="see_all_networks">See all</string>
+
+ <!-- Text for TileService request dialog. This is shown to the user that an app is requesting
+ user approval to add the shown tile to Quick Settings [CHAR LIMIT=NONE] -->
+ <string name="qs_tile_request_dialog_text"><xliff:g id="appName" example="Fake App">%1$s</xliff:g> wants to add the following tile to Quick Settings</string>
+ <!-- Text for TileService request dialog. Text for button for user to approve adding the tile [CHAR LIMIT=20] -->
+ <string name="qs_tile_request_dialog_add">Add tile</string>
+ <!-- Text for TileService request dialog. Text for button for user to reject adding the tile [CHAR LIMIT=20] -->
+ <string name="qs_tile_request_dialog_not_add">Do not add tile</string>
</resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index d254742..7f4866f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -727,6 +727,16 @@
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
</style>
+ <!-- TileService request dialog -->
+ <style name="TileRequestDialog" parent="Theme.SystemUI.QuickSettings.Dialog">
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@drawable/qs_dialog_bg</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:windowCloseOnTouchOutside">true</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+ </style>
+
<!-- USB Contaminant dialog -->
<style name ="USBContaminant" />
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 541ee2c..9445130 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -47,6 +47,7 @@
import com.android.systemui.qs.external.CustomTileStatePersister;
import com.android.systemui.qs.external.TileLifecycleManager;
import com.android.systemui.qs.external.TileServiceKey;
+import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.external.TileServices;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.settings.UserTracker;
@@ -106,6 +107,8 @@
private UserTracker mUserTracker;
private SecureSettings mSecureSettings;
+ private final TileServiceRequestController mTileServiceRequestController;
+
@Inject
public QSTileHost(Context context,
StatusBarIconController iconController,
@@ -122,7 +125,8 @@
UiEventLogger uiEventLogger,
UserTracker userTracker,
SecureSettings secureSettings,
- CustomTileStatePersister customTileStatePersister
+ CustomTileStatePersister customTileStatePersister,
+ TileServiceRequestController.Builder tileServiceRequestControllerBuilder
) {
mIconController = iconController;
mContext = context;
@@ -133,6 +137,7 @@
mQSLogger = qsLogger;
mUiEventLogger = uiEventLogger;
mBroadcastDispatcher = broadcastDispatcher;
+ mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this);
mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID);
mServices = new TileServices(this, bgLooper, mBroadcastDispatcher, userTracker);
@@ -152,6 +157,7 @@
tunerService.addTunable(this, TILES_SETTING);
// AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
mAutoTiles = autoTiles.get();
+ mTileServiceRequestController.init();
});
}
@@ -171,6 +177,7 @@
mServices.destroy();
mPluginManager.removePluginListener(this);
mDumpManager.unregisterDumpable(TAG);
+ mTileServiceRequestController.destroy();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt
new file mode 100644
index 0000000..baf3018
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2021 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.qs.external
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.WindowInsets
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.QSTileView
+import com.android.systemui.qs.tileimpl.QSIconViewImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
+import com.android.systemui.qs.tileimpl.QSTileViewImpl
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/**
+ * Dialog to present to the user to ask for authorization to add a [TileService].
+ */
+class TileRequestDialog(
+ context: Context
+) : SystemUIDialog(context, R.style.TileRequestDialog) {
+
+ companion object {
+ internal val CONTENT_ID = R.id.content
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window?.apply {
+ attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
+ attributes.receiveInsetsIgnoringZOrder = true
+ setLayout(
+ context.resources
+ .getDimensionPixelSize(R.dimen.qs_tile_service_request_dialog_width),
+ WRAP_CONTENT
+ )
+ }
+ }
+
+ /**
+ * Set the data of the tile to add, to show the user.
+ */
+ fun setTileData(tileData: TileData) {
+ val ll = (LayoutInflater
+ .from(context)
+ .inflate(R.layout.tile_service_request_dialog, null)
+ as ViewGroup).apply {
+ requireViewById<TextView>(R.id.text).apply {
+ text = context
+ .getString(R.string.qs_tile_request_dialog_text, tileData.appName)
+ }
+ addView(
+ createTileView(tileData),
+ context.resources.getDimensionPixelSize(
+ R.dimen.qs_tile_service_request_tile_width),
+ context.resources.getDimensionPixelSize(R.dimen.qs_quick_tile_size)
+ )
+ }
+ val spacing = context.resources.getDimensionPixelSize(
+ R.dimen.qs_tile_service_request_content_space
+ )
+ setView(ll, spacing, spacing, spacing, spacing / 2)
+ }
+
+ private fun createTileView(tileData: TileData): QSTileView {
+ val tile = QSTileViewImpl(context, QSIconViewImpl(context), true)
+ val state = QSTile.BooleanState().apply {
+ label = tileData.label
+ icon = tileData.icon?.loadDrawable(context)?.let {
+ QSTileImpl.DrawableIcon(it)
+ } ?: ResourceIcon.get(R.drawable.android)
+ }
+ tile.onStateChanged(state)
+ tile.isSelected = true
+ return tile
+ }
+
+ /**
+ * Data bundle of information to show the user.
+ *
+ * @property appName Name of the app requesting their [TileService] to be added.
+ * @property label Label of the tile.
+ * @property icon Icon for the tile.
+ */
+ data class TileData(
+ val appName: CharSequence,
+ val label: CharSequence,
+ val icon: Icon?
+ )
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
new file mode 100644
index 0000000..e6da234
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2021 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.qs.external
+
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.DialogInterface
+import android.graphics.drawable.Icon
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.QSTileHost
+import com.android.systemui.statusbar.commandline.Command
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.R
+import java.io.PrintWriter
+import java.util.function.Consumer
+import javax.inject.Inject
+
+private const val TAG = "TileServiceRequestController"
+
+/**
+ * Controller to interface between [TileRequestDialog] and [QSTileHost].
+ */
+class TileServiceRequestController constructor(
+ private val qsTileHost: QSTileHost,
+ private val commandRegistry: CommandRegistry,
+ private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsTileHost.context) }
+) {
+
+ companion object {
+ // Temporary return values while there's no API
+ internal const val ADD_TILE = 0
+ internal const val DONT_ADD_TILE = 1
+ internal const val TILE_ALREADY_ADDED = 2
+ internal const val DISMISSED = 3
+ }
+
+ fun init() {
+ commandRegistry.registerCommand("tile-service-add") { TileServiceRequestCommand() }
+ }
+
+ fun destroy() {
+ commandRegistry.unregisterCommand("tile-service-add")
+ }
+
+ private fun addTile(componentName: ComponentName) {
+ qsTileHost.addTile(componentName, true)
+ }
+
+ @VisibleForTesting
+ internal fun requestTileAdd(
+ componentName: ComponentName,
+ appName: CharSequence,
+ label: CharSequence,
+ icon: Icon?,
+ callback: Consumer<Int>
+ ) {
+ if (isTileAlreadyAdded(componentName)) {
+ callback.accept(TILE_ALREADY_ADDED)
+ return
+ }
+ val dialogResponse = object : Consumer<Int> {
+ override fun accept(response: Int) {
+ if (response == ADD_TILE) {
+ addTile(componentName)
+ }
+ callback.accept(response)
+ }
+ }
+ val tileData = TileRequestDialog.TileData(appName, label, icon)
+ createDialog(tileData, dialogResponse).show()
+ }
+
+ private fun createDialog(
+ tileData: TileRequestDialog.TileData,
+ responseHandler: Consumer<Int>
+ ): SystemUIDialog {
+ val dialogClickListener = DialogInterface.OnClickListener { _, which ->
+ if (which == Dialog.BUTTON_POSITIVE) {
+ responseHandler.accept(ADD_TILE)
+ } else {
+ responseHandler.accept(DONT_ADD_TILE)
+ }
+ }
+ return dialogCreator().apply {
+ setTileData(tileData)
+ setShowForAllUsers(true)
+ setCanceledOnTouchOutside(true)
+ setOnCancelListener { responseHandler.accept(DISMISSED) }
+ setPositiveButton(R.string.qs_tile_request_dialog_add, dialogClickListener)
+ setNegativeButton(R.string.qs_tile_request_dialog_not_add, dialogClickListener)
+ }
+ }
+
+ private fun isTileAlreadyAdded(componentName: ComponentName): Boolean {
+ val spec = CustomTile.toSpec(componentName)
+ return qsTileHost.indexOf(spec) != -1
+ }
+
+ inner class TileServiceRequestCommand : Command {
+ override fun execute(pw: PrintWriter, args: List<String>) {
+ val componentName: ComponentName = ComponentName.unflattenFromString(args[0])
+ ?: run {
+ Log.w(TAG, "Malformed componentName ${args[0]}")
+ return
+ }
+ requestTileAdd(componentName, args[1], args[2], null) {
+ Log.d(TAG, "Response: $it")
+ }
+ }
+
+ override fun help(pw: PrintWriter) {
+ pw.println("Usage: adb shell cmd statusbar tile-service-add " +
+ "<componentName> <appName> <label>")
+ }
+ }
+
+ @SysUISingleton
+ class Builder @Inject constructor(
+ private val commandRegistry: CommandRegistry
+ ) {
+ fun create(qsTileHost: QSTileHost): TileServiceRequestController {
+ return TileServiceRequestController(qsTileHost, commandRegistry)
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 222539d..ee5e4df 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -286,6 +286,10 @@
return labelContainer
}
+ override fun getLabel(): View {
+ return label
+ }
+
override fun getSecondaryLabel(): View {
return secondaryLabel
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 109721f..1ed34d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -48,6 +48,7 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.external.CustomTileStatePersister;
+import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSFactoryImpl;
import com.android.systemui.settings.UserTracker;
@@ -95,6 +96,10 @@
private KeyguardBypassController mBypassController;
@Mock
private FalsingManager mFalsingManager;
+ @Mock
+ private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder;
+ @Mock
+ private TileServiceRequestController mTileServiceRequestController;
public QSFragmentTest() {
super(QSFragment.class);
@@ -108,6 +113,9 @@
when(mQsComponentFactory.create(any(QSFragment.class))).thenReturn(mQsFragmentComponent);
when(mQsFragmentComponent.getQSPanelController()).thenReturn(mQSPanelController);
+ when(mTileServiceRequestControllerBuilder.create(any()))
+ .thenReturn(mTileServiceRequestController);
+
mMockMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class);
mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE,
new LayoutInflaterBuilder(mContext)
@@ -136,7 +144,8 @@
() -> mock(AutoTileManager.class), mock(DumpManager.class),
mock(BroadcastDispatcher.class), Optional.of(mock(StatusBar.class)),
mock(QSLogger.class), mock(UiEventLogger.class), mock(UserTracker.class),
- mock(SecureSettings.class), mock(CustomTileStatePersister.class));
+ mock(SecureSettings.class), mock(CustomTileStatePersister.class),
+ mTileServiceRequestControllerBuilder);
qs.setHost(host);
qs.setListening(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 9e97f80..bf9d4b1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -21,6 +21,7 @@
import static junit.framework.Assert.assertTrue;
import static junit.framework.TestCase.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -59,6 +60,7 @@
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.external.CustomTileStatePersister;
import com.android.systemui.qs.external.TileServiceKey;
+import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.settings.UserTracker;
@@ -124,6 +126,10 @@
private SecureSettings mSecureSettings;
@Mock
private CustomTileStatePersister mCustomTileStatePersister;
+ @Mock
+ private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder;
+ @Mock
+ private TileServiceRequestController mTileServiceRequestController;
private Handler mHandler;
private TestableLooper mLooper;
@@ -134,10 +140,14 @@
MockitoAnnotations.initMocks(this);
mLooper = TestableLooper.get(this);
mHandler = new Handler(mLooper.getLooper());
+
+ when(mTileServiceRequestControllerBuilder.create(any()))
+ .thenReturn(mTileServiceRequestController);
+
mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mHandler,
mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpManager,
mBroadcastDispatcher, mStatusBar, mQSLogger, mUiEventLogger, mUserTracker,
- mSecureSettings, mCustomTileStatePersister);
+ mSecureSettings, mCustomTileStatePersister, mTileServiceRequestControllerBuilder);
setUpTileFactory();
when(mSecureSettings.getStringForUser(eq(QSTileHost.TILES_SETTING), anyInt()))
@@ -371,11 +381,12 @@
Provider<AutoTileManager> autoTiles, DumpManager dumpManager,
BroadcastDispatcher broadcastDispatcher, StatusBar statusBar, QSLogger qsLogger,
UiEventLogger uiEventLogger, UserTracker userTracker,
- SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister) {
+ SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister,
+ TileServiceRequestController.Builder tileServiceRequestControllerBuilder) {
super(context, iconController, defaultFactory, mainHandler, bgLooper, pluginManager,
tunerService, autoTiles, dumpManager, broadcastDispatcher,
Optional.of(statusBar), qsLogger, uiEventLogger, userTracker, secureSettings,
- customTileStatePersister);
+ customTileStatePersister, tileServiceRequestControllerBuilder);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt
new file mode 100644
index 0000000..d49673d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 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.qs.external
+
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTileView
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class TileRequestDialogTest : SysuiTestCase() {
+
+ companion object {
+ private const val APP_NAME = "App name"
+ private const val LABEL = "Label"
+ }
+
+ private lateinit var dialog: TileRequestDialog
+
+ @Before
+ fun setUp() {
+ // Create in looper so we can make sure that the tile is fully updated
+ TestableLooper.get(this).runWithLooper {
+ dialog = TileRequestDialog(mContext)
+ }
+ }
+
+ @After
+ fun teardown() {
+ if (this::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun useCorrectTheme() {
+ assertThat(dialog.context.themeResId).isEqualTo(R.style.TileRequestDialog)
+ }
+
+ @Test
+ fun setTileData_hasCorrectViews() {
+ val icon = Icon.createWithResource(mContext, R.drawable.cloud)
+ val tileData = TileRequestDialog.TileData(APP_NAME, LABEL, icon)
+
+ dialog.setTileData(tileData)
+ dialog.show()
+
+ val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
+
+ assertThat(content.childCount).isEqualTo(2)
+ assertThat(content.getChildAt(0)).isInstanceOf(TextView::class.java)
+ assertThat(content.getChildAt(1)).isInstanceOf(QSTileView::class.java)
+ }
+
+ @Test
+ fun setTileData_hasCorrectAppName() {
+ val icon = Icon.createWithResource(mContext, R.drawable.cloud)
+ val tileData = TileRequestDialog.TileData(APP_NAME, LABEL, icon)
+
+ dialog.setTileData(tileData)
+ dialog.show()
+
+ val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
+ val text = content.getChildAt(0) as TextView
+ assertThat(text.text.toString()).contains(APP_NAME)
+ }
+
+ @Test
+ fun setTileData_hasCorrectLabel() {
+ val icon = Icon.createWithResource(mContext, R.drawable.cloud)
+ val tileData = TileRequestDialog.TileData(APP_NAME, LABEL, icon)
+
+ dialog.setTileData(tileData)
+ dialog.show()
+
+ TestableLooper.get(this).processAllMessages()
+
+ val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
+ val tile = content.getChildAt(1) as QSTileView
+ assertThat((tile.label as TextView).text.toString()).isEqualTo(LABEL)
+ }
+
+ @Test
+ fun setTileData_hasIcon() {
+ val icon = Icon.createWithResource(mContext, R.drawable.cloud)
+ val tileData = TileRequestDialog.TileData(APP_NAME, LABEL, icon)
+
+ dialog.setTileData(tileData)
+ dialog.show()
+
+ TestableLooper.get(this).processAllMessages()
+
+ val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
+ val tile = content.getChildAt(1) as QSTileView
+ assertThat((tile.icon.iconView as ImageView).drawable).isNotNull()
+ }
+
+ @Test
+ fun setTileData_nullIcon_hasIcon() {
+ val tileData = TileRequestDialog.TileData(APP_NAME, LABEL, null)
+
+ dialog.setTileData(tileData)
+ dialog.show()
+
+ TestableLooper.get(this).processAllMessages()
+
+ val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
+ val tile = content.getChildAt(1) as QSTileView
+ assertThat((tile.icon.iconView as ImageView).drawable).isNotNull()
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt
new file mode 100644
index 0000000..ce8e58ca
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2021 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.qs.external
+
+import android.content.ComponentName
+import android.content.DialogInterface
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.QSTileHost
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.function.Consumer
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TileServiceRequestControllerTest : SysuiTestCase() {
+
+ companion object {
+ private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
+ private const val TEST_APP_NAME = "App"
+ private const val TEST_LABEL = "Label"
+ }
+
+ @Mock
+ private lateinit var tileRequestDialog: TileRequestDialog
+ @Mock
+ private lateinit var qsTileHost: QSTileHost
+ @Mock
+ private lateinit var commandRegistry: CommandRegistry
+ @Mock
+ private lateinit var icon: Icon
+
+ private lateinit var controller: TileServiceRequestController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ // Tile not present by default
+ `when`(qsTileHost.indexOf(anyString())).thenReturn(-1)
+
+ controller = TileServiceRequestController(qsTileHost, commandRegistry) {
+ tileRequestDialog
+ }
+
+ controller.init()
+ }
+
+ @Test
+ fun requestTileAdd_dataIsPassedToDialog() {
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
+
+ verify(tileRequestDialog).setTileData(
+ TileRequestDialog.TileData(TEST_APP_NAME, TEST_LABEL, icon)
+ )
+ }
+
+ @Test
+ fun tileAlreadyAdded_correctResult() {
+ `when`(qsTileHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
+
+ val callback = Callback()
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
+
+ assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED)
+ verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
+ }
+
+ @Test
+ fun showAllUsers_set() {
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
+ verify(tileRequestDialog).setShowForAllUsers(true)
+ }
+
+ @Test
+ fun cancelOnTouchOutside_set() {
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
+ verify(tileRequestDialog).setCanceledOnTouchOutside(true)
+ }
+
+ @Test
+ fun cancelListener_dismissResult() {
+ val cancelListenerCaptor =
+ ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
+
+ val callback = Callback()
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
+ verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
+
+ cancelListenerCaptor.value.onCancel(tileRequestDialog)
+ assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
+ verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
+ }
+
+ @Test
+ fun positiveActionListener_tileAddedResult() {
+ val clickListenerCaptor =
+ ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
+
+ val callback = Callback()
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
+ verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
+
+ clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
+
+ assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.ADD_TILE)
+ verify(qsTileHost).addTile(TEST_COMPONENT, /* end */ true)
+ }
+
+ @Test
+ fun negativeActionListener_tileNotAddedResult() {
+ val clickListenerCaptor =
+ ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
+
+ val callback = Callback()
+ controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
+ verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor))
+
+ clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
+
+ assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DONT_ADD_TILE)
+ verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
+ }
+
+ private class Callback : Consumer<Int> {
+ var lastAccepted: Int? = null
+ private set
+ override fun accept(t: Int) {
+ lastAccepted = t
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 2b18404..e027ab7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -98,12 +98,20 @@
private UserTracker mUserTracker;
@Mock
private SecureSettings mSecureSettings;
+ @Mock
+ private TileServiceRequestController.Builder mTileServiceRequestControllerBuilder;
+ @Mock
+ private TileServiceRequestController mTileServiceRequestController;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mDependency.injectMockDependency(BluetoothController.class);
mManagers = new ArrayList<>();
+
+ when(mTileServiceRequestControllerBuilder.create(any()))
+ .thenReturn(mTileServiceRequestController);
+
QSTileHost host = new QSTileHost(mContext,
mStatusBarIconController,
mQSFactory,
@@ -119,7 +127,8 @@
mUiEventLogger,
mUserTracker,
mSecureSettings,
- mock(CustomTileStatePersister.class));
+ mock(CustomTileStatePersister.class),
+ mTileServiceRequestControllerBuilder);
mTileService = new TestTileServices(host, Looper.getMainLooper(), mBroadcastDispatcher,
mUserTracker);
}