[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:
22
packages/SystemUI/res/drawable/media_ttt_chip_background.xml
Normal file
22
packages/SystemUI/res/drawable/media_ttt_chip_background.xml
Normal 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>
|
||||
36
packages/SystemUI/res/layout/media_ttt_chip.xml
Normal file
36
packages/SystemUI/res/layout/media_ttt_chip.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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] -->
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user