[Media TTT] Add three different states for the media TTT chip and allow

each of them to be displayed with the correct text via adb.

For example, to display the "Move closer to..." chip state for a device
named "Tablet", run the adb command `adb shell cmd statusbar
media-ttt-chip-add Tablet MOVE_CLOSER_TO_TRANSFER`.

App icon, loading icon, and undo button coming in future CLs.
Screenshots of the chip with loading and undo are in
https://b.corp.google.com/issues/203800327#comment3.

Bug: 203800327
Test: `adb shell cmd statusbar
media-ttt-chip-add Tablet MOVE_CLOSER_TO_TRANSFER` shows a chip with the
text "Move closer to play on Tablet"
Test: `adb shell cmd statusbar
media-ttt-chip-add Tablet TRANSFER_INITIATED` shows a chip with the text
"Playing on Tablet" (will have a loading icon in future)
Test: `adb shell cmd statusbar
media-ttt-chip-add Tablet TRANSFER_SUCCEEDED` shows a chip with the text
"Playing on Tablet" (will have an undo button in future)
Test: MediaTttChipControllerTest

Change-Id: Icc8f5561fff6d862c4eedaa5b454c35aa827bcaf
This commit is contained in:
Caitlin Cassidy
2021-11-30 20:10:33 +00:00
parent 77243f982a
commit b38287430b
7 changed files with 186 additions and 23 deletions

View File

@@ -0,0 +1,22 @@
<!--
~ 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<solid android:color="?androidprv:attr/colorSurface" />
<corners android:radius="32dp" />
</shape>

View File

@@ -0,0 +1,36 @@
<!--
~ 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:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/media_ttt_chip_outer_padding"
android:background="@drawable/media_ttt_chip_background"
android:layout_marginTop="50dp"
android:clipToPadding="false"
android:gravity="center_vertical"
>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/media_ttt_text_size"
android:textColor="?android:attr/textColorPrimary"
/>
</LinearLayout>

View File

@@ -976,6 +976,10 @@
<dimen name="qs_aa_media_rec_album_margin_vert">4dp</dimen>
<dimen name="qq_aa_media_rec_header_text_size">16sp</dimen>
<!-- Media tap-to-transfer chip -->
<dimen name="media_ttt_chip_outer_padding">16dp</dimen>
<dimen name="media_ttt_text_size">16sp</dimen>
<!-- Window magnification -->
<dimen name="magnification_border_drag_size">35dp</dimen>
<dimen name="magnification_outer_border_margin">15dp</dimen>

View File

@@ -2173,6 +2173,14 @@
<!-- Description for Smartspace recommendation's media item which doesn't have artist info, including information for the media's title and the source app [CHAR LIMIT=NONE]-->
<string name="controls_media_smartspace_rec_item_no_artist_description">Play <xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> from <xliff:g id="app_label" example="Spotify">%2$s</xliff:g></string>
<!--- ****** Media tap-to-transfer ****** -->
<!-- Text for a button to undo the media transfer. [CHAR LIMIT=20] -->
<string name="media_transfer_undo">Undo</string>
<!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play music on the different device. [CHAR LIMIT=75] -->
<string name="media_move_closer_to_transfer">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] -->
<string name="media_transfer_playing">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string>
<!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
<string name="controls_error_timeout">Inactive, check app</string>
<!-- Error message indicating that the control is no longer available in the application [CHAR LIMIT=30] -->

View File

@@ -83,6 +83,6 @@ public interface MediaModule {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttChipController(context, commandRegistry, windowManager));
return Optional.of(new MediaTttChipController(commandRegistry, context, windowManager));
}
}

View File

@@ -19,9 +19,13 @@ package com.android.systemui.media.taptotransfer
import android.content.Context
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -36,8 +40,8 @@ import javax.inject.Inject
*/
@SysUISingleton
class MediaTttChipController @Inject constructor(
context: Context,
commandRegistry: CommandRegistry,
private val context: Context,
private val windowManager: WindowManager,
) {
init {
@@ -46,9 +50,9 @@ class MediaTttChipController @Inject constructor(
}
private val windowLayoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
gravity = Gravity.CENTER_HORIZONTAL
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
title = "Media Tap-To-Transfer Chip View"
flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -57,29 +61,61 @@ class MediaTttChipController @Inject constructor(
setTrustedOverlay()
}
// TODO(b/203800327): Create a layout that matches UX.
private val chipView: TextView = TextView(context).apply {
text = "Media Tap-To-Transfer Chip"
}
/** The chip view currently being displayed. Null if the chip is not being displayed. */
private var chipView: LinearLayout? = null
private var chipDisplaying: Boolean = false
private fun displayChip(chipType: ChipType, otherDeviceName: String) {
val oldChipView = chipView
if (chipView == null) {
chipView = LayoutInflater
.from(context)
.inflate(R.layout.media_ttt_chip, null) as LinearLayout
}
val currentChipView = chipView!!
private fun addChip() {
if (chipDisplaying) { return }
windowManager.addView(chipView, windowLayoutParams)
chipDisplaying = true
// Text
currentChipView.requireViewById<TextView>(R.id.text).apply {
text = context.getString(chipType.chipText, otherDeviceName)
}
if (oldChipView == null) {
windowManager.addView(chipView, windowLayoutParams)
}
}
private fun removeChip() {
if (!chipDisplaying) { return }
if (chipView == null) { return }
windowManager.removeView(chipView)
chipDisplaying = false
chipView = null
}
@VisibleForTesting
enum class ChipType(
@StringRes internal val chipText: Int
) {
MOVE_CLOSER_TO_TRANSFER(R.string.media_move_closer_to_transfer),
TRANSFER_INITIATED(R.string.media_transfer_playing),
TRANSFER_SUCCEEDED(R.string.media_transfer_playing),
}
inner class AddChipCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) = addChip()
override fun execute(pw: PrintWriter, args: List<String>) {
val chipTypeArg = args[1]
ChipType.values().forEach {
if (it.name == chipTypeArg) {
displayChip(it, otherDeviceName = args[0])
return
}
}
pw.println("Chip type must be one of " +
ChipType.values().map { it.name }.reduce { acc, s -> "$acc, $s" })
}
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_TAG")
pw.println(
"Usage: adb shell cmd statusbar $ADD_CHIP_COMMAND_TAG <deviceName> <chipType>"
)
}
}

View File

@@ -16,14 +16,20 @@
package com.android.systemui.media.taptotransfer
import android.view.View
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
@@ -48,7 +54,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
mediaTttChipController = MediaTttChipController(context, commandRegistry, windowManager)
mediaTttChipController = MediaTttChipController(commandRegistry, context, windowManager)
}
@Test(expected = IllegalStateException::class)
@@ -71,24 +77,24 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun addChipCommand_chipAdded() {
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG))
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
verify(windowManager).addView(any(), any())
}
@Test
fun addChipCommand_twice_chipNotAddedTwice() {
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG))
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
reset(windowManager)
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG))
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
verify(windowManager, never()).addView(any(), any())
}
@Test
fun removeChipCommand_chipRemoved() {
// First, add the chip
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.ADD_CHIP_COMMAND_TAG))
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
// Then, remove it
commandRegistry.onShellCommand(pw, arrayOf(MediaTttChipController.REMOVE_CHIP_COMMAND_TAG))
@@ -103,6 +109,55 @@ class MediaTttChipControllerTest : SysuiTestCase() {
verify(windowManager, never()).removeView(any())
}
@Test
fun moveCloserToTransfer_chipTextContainsDeviceName() {
commandRegistry.onShellCommand(pw, getMoveCloserToTransferCommand())
assertThat(getChipText()).contains(DEVICE_NAME)
}
@Test
fun transferInitiated_chipTextContainsDeviceName() {
commandRegistry.onShellCommand(pw, getTransferInitiatedCommand())
assertThat(getChipText()).contains(DEVICE_NAME)
}
@Test
fun transferSucceeded_chipTextContainsDeviceName() {
commandRegistry.onShellCommand(pw, getTransferSucceededCommand())
assertThat(getChipText()).contains(DEVICE_NAME)
}
private fun getMoveCloserToTransferCommand(): Array<String> =
arrayOf(
MediaTttChipController.ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MediaTttChipController.ChipType.MOVE_CLOSER_TO_TRANSFER.name
)
private fun getTransferInitiatedCommand(): Array<String> =
arrayOf(
MediaTttChipController.ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MediaTttChipController.ChipType.TRANSFER_INITIATED.name
)
private fun getTransferSucceededCommand(): Array<String> =
arrayOf(
MediaTttChipController.ADD_CHIP_COMMAND_TAG,
DEVICE_NAME,
MediaTttChipController.ChipType.TRANSFER_SUCCEEDED.name
)
private fun getChipText(): String {
val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any())
val chipView = viewCaptor.value as LinearLayout
return (chipView.requireViewById(R.id.text) as TextView).text as String
}
class EmptyCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
}
@@ -111,3 +166,5 @@ class MediaTttChipControllerTest : SysuiTestCase() {
}
}
}
private const val DEVICE_NAME = "My Tablet"