[Media TTT] Add the app icon to the view.

Fixes: 209796699
Test: manual (icon shows on the command `adb shell cmd statusbar
media-ttt-chip-add Tablet <any state>`
Test: MediaTttChipControllerTest

Change-Id: I59f243ed28521ce7ce0c07a0e92a5ac4e37c6599
This commit is contained in:
Caitlin Cassidy
2021-12-16 17:26:41 +00:00
parent 3398ba9c55
commit e087e12997
8 changed files with 107 additions and 43 deletions

View File

@@ -26,6 +26,13 @@
android:gravity="center_vertical"
>
<com.android.internal.widget.CachingIconView
android:id="@+id/app_icon"
android:layout_width="@dimen/media_ttt_icon_size"
android:layout_height="@dimen/media_ttt_icon_size"
android:layout_marginEnd="12dp"
/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
@@ -37,8 +44,8 @@
<ProgressBar
android:id="@+id/loading"
android:indeterminate="true"
android:layout_width="@dimen/media_ttt_icon_size"
android:layout_height="@dimen/media_ttt_icon_size"
android:layout_width="@dimen/media_ttt_loading_size"
android:layout_height="@dimen/media_ttt_loading_size"
android:layout_marginStart="12dp"
android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant"
style="?android:attr/progressBarStyleSmall"

View File

@@ -980,7 +980,8 @@
<!-- Media tap-to-transfer chip -->
<dimen name="media_ttt_chip_outer_padding">16dp</dimen>
<dimen name="media_ttt_text_size">16sp</dimen>
<dimen name="media_ttt_icon_size">16dp</dimen>
<dimen name="media_ttt_icon_size">24dp</dimen>
<dimen name="media_ttt_loading_size">20dp</dimen>
<dimen name="media_ttt_undo_button_vertical_padding">8dp</dimen>
<dimen name="media_ttt_undo_button_vertical_negative_margin">-8dp</dimen>

View File

@@ -99,12 +99,13 @@ public interface MediaModule {
static Optional<MediaTttCommandLineHelper> providesMediaTttCommandLineHelper(
MediaTttFlags mediaTttFlags,
CommandRegistry commandRegistry,
Context context,
MediaTttChipController mediaTttChipController,
@Main DelayableExecutor mainExecutor) {
if (!mediaTttFlags.isMediaTttEnabled()) {
return Optional.empty();
}
return Optional.of(new MediaTttCommandLineHelper(
commandRegistry, mediaTttChipController, mainExecutor));
commandRegistry, context, mediaTttChipController, mainExecutor));
}
}

View File

@@ -25,6 +25,7 @@ import android.view.View
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import com.android.internal.widget.CachingIconView
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -74,6 +75,11 @@ class MediaTttChipController @Inject constructor(
}
val currentChipView = chipView!!
// App icon
currentChipView.findViewById<CachingIconView>(R.id.app_icon).apply {
this.setImageDrawable(chipState.appIconDrawable)
}
// Text
currentChipView.requireViewById<TextView>(R.id.text).apply {
text = context.getString(chipState.chipText, chipState.otherDeviceName)
@@ -128,7 +134,11 @@ class MediaTttChipController @Inject constructor(
val undoRunnable = chipState.future.get(TRANSFER_TIMEOUT_SECONDS, TimeUnit.SECONDS)
// Make UI changes on the main thread
mainExecutor.execute {
displayChip(TransferSucceeded(chipState.otherDeviceName, undoRunnable))
displayChip(
TransferSucceeded(
chipState.otherDeviceName, chipState.appIconDrawable, undoRunnable
)
)
}
} catch (ex: Exception) {
// TODO(b/203800327): Maybe show a failure chip here if UX decides we need one.

View File

@@ -16,6 +16,7 @@
package com.android.systemui.media.taptotransfer
import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
import com.android.systemui.R
import java.util.concurrent.Future
@@ -26,12 +27,15 @@ import java.util.concurrent.Future
*
* This is a sealed class where each subclass represents a specific chip state. Each subclass can
* contain additional information that is necessary for only that state.
*
* @property chipText a string resource for the text that the chip should display.
* @property otherDeviceName the name of the other device involved in the transfer.
* @property appIconDrawable a drawable representing the icon of the app playing the media.
*/
sealed class MediaTttChipState(
/** A string resource for the text that the chip should display. */
@StringRes internal val chipText: Int,
/** The name of the other device involved in the transfer. */
internal val otherDeviceName: String
internal val otherDeviceName: String,
internal val appIconDrawable: Drawable,
)
/**
@@ -39,8 +43,9 @@ sealed class MediaTttChipState(
* The chip will instruct the user to move closer in order to initiate the transfer.
*/
class MoveCloserToTransfer(
otherDeviceName: String
) : MediaTttChipState(R.string.media_move_closer_to_transfer, otherDeviceName)
otherDeviceName: String,
appIconDrawable: Drawable
) : MediaTttChipState(R.string.media_move_closer_to_transfer, otherDeviceName, appIconDrawable)
/**
* A state representing that a transfer has been initiated (but not completed).
@@ -52,8 +57,9 @@ class MoveCloserToTransfer(
*/
class TransferInitiated(
otherDeviceName: String,
appIconDrawable: Drawable,
val future: Future<Runnable?>
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName)
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName, appIconDrawable)
/**
* A state representing that a transfer has been successfully completed.
@@ -63,5 +69,6 @@ class TransferInitiated(
*/
class TransferSucceeded(
otherDeviceName: String,
appIconDrawable: Drawable,
val undoRunnable: Runnable? = null
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName)
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName, appIconDrawable)

View File

@@ -16,8 +16,11 @@
package com.android.systemui.media.taptotransfer
import android.content.Context
import android.graphics.drawable.Icon
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.commandline.Command
@@ -34,9 +37,13 @@ import javax.inject.Inject
@SysUISingleton
class MediaTttCommandLineHelper @Inject constructor(
commandRegistry: CommandRegistry,
context: Context,
private val mediaTttChipController: MediaTttChipController,
@Main private val mainExecutor: DelayableExecutor,
) {
private val appIconDrawable =
Icon.createWithResource(context, R.drawable.ic_cake).loadDrawable(context)
init {
commandRegistry.registerCommand(ADD_CHIP_COMMAND_TAG) { AddChipCommand() }
commandRegistry.registerCommand(REMOVE_CHIP_COMMAND_TAG) { RemoveChipCommand() }
@@ -47,19 +54,21 @@ class MediaTttCommandLineHelper @Inject constructor(
val otherDeviceName = args[0]
when (args[1]) {
MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME -> {
mediaTttChipController.displayChip(MoveCloserToTransfer(otherDeviceName))
mediaTttChipController.displayChip(
MoveCloserToTransfer(otherDeviceName, appIconDrawable)
)
}
TRANSFER_INITIATED_COMMAND_NAME -> {
val futureTask = FutureTask { fakeUndoRunnable }
mediaTttChipController.displayChip(
TransferInitiated(otherDeviceName, futureTask)
TransferInitiated(otherDeviceName, appIconDrawable, futureTask)
)
mainExecutor.executeDelayed({ futureTask.run() }, FUTURE_WAIT_TIME)
}
TRANSFER_SUCCEEDED_COMMAND_NAME -> {
mediaTttChipController.displayChip(
TransferSucceeded(otherDeviceName, fakeUndoRunnable)
TransferSucceeded(otherDeviceName, appIconDrawable, fakeUndoRunnable)
)
}
else -> {

View File

@@ -16,8 +16,11 @@
package com.android.systemui.media.taptotransfer
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.view.View
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.test.filters.SmallTest
@@ -36,10 +39,11 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.concurrent.Future
@SmallTest
class MediaTttChipControllerTest : SysuiTestCase() {
private lateinit var appIconDrawable: Drawable
private lateinit var fakeMainClock: FakeSystemClock
private lateinit var fakeMainExecutor: FakeExecutor
private lateinit var fakeBackgroundClock: FakeSystemClock
@@ -53,6 +57,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
appIconDrawable = Icon.createWithResource(context, R.drawable.ic_cake).loadDrawable(context)
fakeMainClock = FakeSystemClock()
fakeMainExecutor = FakeExecutor(fakeMainClock)
fakeBackgroundClock = FakeSystemClock()
@@ -64,24 +69,24 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun displayChip_chipAdded() {
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(moveCloserToTransfer())
verify(windowManager).addView(any(), any())
}
@Test
fun displayChip_twice_chipNotAddedTwice() {
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(moveCloserToTransfer())
reset(windowManager)
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(moveCloserToTransfer())
verify(windowManager, never()).addView(any(), any())
}
@Test
fun removeChip_chipRemoved() {
// First, add the chip
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(moveCloserToTransfer())
// Then, remove it
mediaTttChipController.removeChip()
@@ -97,24 +102,26 @@ class MediaTttChipControllerTest : SysuiTestCase() {
}
@Test
fun moveCloserToTransfer_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
fun moveCloserToTransfer_appIcon_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
mediaTttChipController.displayChip(moveCloserToTransfer())
val chipView = getChipView()
assertThat(chipView.getAppIconDrawable()).isEqualTo(appIconDrawable)
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
}
@Test
fun transferInitiated_futureNotResolvedYet_loadingIcon_noUndo() {
fun transferInitiated_futureNotResolvedYet_appIcon_loadingIcon_noUndo() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, future))
mediaTttChipController.displayChip(transferInitiated(future))
// Don't resolve the future in any way and don't run our executors
// Assert we're still in the loading state
val chipView = getChipView()
assertThat(chipView.getAppIconDrawable()).isEqualTo(appIconDrawable)
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
@@ -125,7 +132,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
val undoRunnable = Runnable { }
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, future))
mediaTttChipController.displayChip(transferInitiated(future))
future.set(undoRunnable)
fakeBackgroundExecutor.advanceClockToLast()
@@ -146,7 +153,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
fun transferInitiated_futureCancelled_chipRemoved() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, future))
mediaTttChipController.displayChip(transferInitiated(future))
future.cancel(true)
fakeBackgroundExecutor.advanceClockToLast()
@@ -163,7 +170,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun transferInitiated_futureNotResolvedAfterTimeout_chipRemoved() {
val future: SettableFuture<Runnable?> = SettableFuture.create()
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, future))
mediaTttChipController.displayChip(transferInitiated(future))
// We won't set anything on the future, but we will still run the executors so that we're
// waiting on the future resolving. If we have a bug in our code, then this test will time
@@ -180,22 +187,28 @@ class MediaTttChipControllerTest : SysuiTestCase() {
}
@Test
fun transferSucceededNullUndoRunnable_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME, undoRunnable = null))
fun transferSucceeded_appIcon_chipTextContainsDeviceName_noLoadingIcon() {
mediaTttChipController.displayChip(transferSucceeded())
val chipView = getChipView()
assertThat(chipView.getAppIconDrawable()).isEqualTo(appIconDrawable)
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
}
@Test
fun transferSucceededNullUndoRunnable_noUndo() {
mediaTttChipController.displayChip(transferSucceeded(undoRunnable = null))
val chipView = getChipView()
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
}
@Test
fun transferSucceededWithUndoRunnable_chipTextContainsDeviceName_noLoadingIcon_undoWithClick() {
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME) { })
fun transferSucceededWithUndoRunnable_undoWithClick() {
mediaTttChipController.displayChip(transferSucceeded { })
val chipView = getChipView()
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
}
@@ -205,7 +218,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
var runnableRun = false
val runnable = Runnable { runnableRun = true }
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME, runnable))
mediaTttChipController.displayChip(transferSucceeded(undoRunnable = runnable))
getChipView().getUndoButton().performClick()
assertThat(runnableRun).isTrue()
@@ -213,36 +226,39 @@ class MediaTttChipControllerTest : SysuiTestCase() {
@Test
fun changeFromCloserToTransferToTransferInitiated_loadingIconAppears() {
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, TEST_FUTURE))
mediaTttChipController.displayChip(moveCloserToTransfer())
mediaTttChipController.displayChip(transferInitiated())
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
}
@Test
fun changeFromTransferInitiatedToTransferSucceeded_loadingIconDisappears() {
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, TEST_FUTURE))
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
mediaTttChipController.displayChip(transferInitiated())
mediaTttChipController.displayChip(transferSucceeded())
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
}
@Test
fun changeFromTransferInitiatedToTransferSucceeded_undoButtonAppears() {
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, TEST_FUTURE))
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME) { })
mediaTttChipController.displayChip(transferInitiated())
mediaTttChipController.displayChip(transferSucceeded { })
assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
}
@Test
fun changeFromTransferSucceededToMoveCloser_undoButtonDisappears() {
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
mediaTttChipController.displayChip(transferSucceeded())
mediaTttChipController.displayChip(moveCloserToTransfer())
assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
}
private fun LinearLayout.getAppIconDrawable(): Drawable =
(this.requireViewById<ImageView>(R.id.app_icon)).drawable
private fun LinearLayout.getChipText(): String =
(this.requireViewById<TextView>(R.id.text)).text as String
@@ -256,6 +272,19 @@ class MediaTttChipControllerTest : SysuiTestCase() {
verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as LinearLayout
}
/** Helper method providing default parameters to not clutter up the tests. */
private fun moveCloserToTransfer() = MoveCloserToTransfer(DEVICE_NAME, appIconDrawable)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferInitiated(
future: Future<Runnable?> = TEST_FUTURE
) = TransferInitiated(DEVICE_NAME, appIconDrawable, future)
/** Helper method providing default parameters to not clutter up the tests. */
private fun transferSucceeded(
undoRunnable: Runnable? = null
) = TransferSucceeded(DEVICE_NAME, appIconDrawable, undoRunnable)
}
private const val DEVICE_NAME = "My Tablet"

View File

@@ -49,7 +49,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
mediaTttCommandLineHelper =
MediaTttCommandLineHelper(
commandRegistry, mediaTttChipController, FakeExecutor(FakeSystemClock())
commandRegistry, context, mediaTttChipController, FakeExecutor(FakeSystemClock())
)
}