Merge "Add dialog and controller for TileServiceRequest"

This commit is contained in:
Fabian Kozynski
2021-08-18 18:38:39 +00:00
committed by Android (Google) Code Review
14 changed files with 660 additions and 6 deletions

View File

@@ -60,6 +60,10 @@ public abstract class QSTileView extends LinearLayout {
public abstract int getDetailY();
public View getLabel() {
return null;
}
public View getLabelContainer() {
return null;
}

View File

@@ -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>

View File

@@ -1634,4 +1634,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>

View File

@@ -3024,4 +3024,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>

View File

@@ -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" />

View File

@@ -47,6 +47,7 @@ import com.android.systemui.qs.external.CustomTile;
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 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
private UserTracker mUserTracker;
private SecureSettings mSecureSettings;
private final TileServiceRequestController mTileServiceRequestController;
@Inject
public QSTileHost(Context context,
StatusBarIconController iconController,
@@ -122,7 +125,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
UiEventLogger uiEventLogger,
UserTracker userTracker,
SecureSettings secureSettings,
CustomTileStatePersister customTileStatePersister
CustomTileStatePersister customTileStatePersister,
TileServiceRequestController.Builder tileServiceRequestControllerBuilder
) {
mIconController = iconController;
mContext = context;
@@ -133,6 +137,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
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 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
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 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
mServices.destroy();
mPluginManager.removePluginListener(this);
mDumpManager.unregisterDumpable(TAG);
mTileServiceRequestController.destroy();
}
@Override

View File

@@ -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?
)
}

View File

@@ -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)
}
}
}

View File

@@ -286,6 +286,10 @@ open class QSTileViewImpl @JvmOverloads constructor(
return labelContainer
}
override fun getLabel(): View {
return label
}
override fun getSecondaryLabel(): View {
return secondaryLabel
}

View File

@@ -48,6 +48,7 @@ import com.android.systemui.plugins.FalsingManager;
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 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
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 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
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 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
() -> 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);

View File

@@ -21,6 +21,7 @@ import static junit.framework.Assert.assertEquals;
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.plugins.statusbar.StatusBarStateController;
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 @@ public class QSTileHostTest extends SysuiTestCase {
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 @@ public class QSTileHostTest extends SysuiTestCase {
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 @@ public class QSTileHostTest extends SysuiTestCase {
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

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -98,12 +98,20 @@ public class TileServicesTest extends SysuiTestCase {
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 @@ public class TileServicesTest extends SysuiTestCase {
mUiEventLogger,
mUserTracker,
mSecureSettings,
mock(CustomTileStatePersister.class));
mock(CustomTileStatePersister.class),
mTileServiceRequestControllerBuilder);
mTileService = new TestTileServices(host, Looper.getMainLooper(), mBroadcastDispatcher,
mUserTracker);
}