[Media TTT] When a transfer has been initiated, listen to a future so
that we can update the chip to succeeded/failed state once the future completes. Bug: 203800665 Test: MediaTttChipControllerTest Test: `adb shell cmd statusbar media-ttt-chip-add Tablet TransferInitiated` should show a loading icon for 2 seconds, then transition to showing an undo button. Change-Id: Ic01e85c9f66f8cd8624c4fc664787b1a9904742c
This commit is contained in:
@@ -20,6 +20,8 @@ import android.content.Context;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.media.MediaDataManager;
|
||||
import com.android.systemui.media.MediaHierarchyManager;
|
||||
import com.android.systemui.media.MediaHost;
|
||||
@@ -28,8 +30,10 @@ import com.android.systemui.media.taptotransfer.MediaTttChipController;
|
||||
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
|
||||
import com.android.systemui.media.taptotransfer.MediaTttFlags;
|
||||
import com.android.systemui.statusbar.commandline.CommandRegistry;
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
@@ -79,11 +83,14 @@ public interface MediaModule {
|
||||
static Optional<MediaTttChipController> providesMediaTttChipController(
|
||||
MediaTttFlags mediaTttFlags,
|
||||
Context context,
|
||||
WindowManager windowManager) {
|
||||
WindowManager windowManager,
|
||||
@Main Executor mainExecutor,
|
||||
@Background Executor backgroundExecutor) {
|
||||
if (!mediaTttFlags.isMediaTttEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new MediaTttChipController(context, windowManager));
|
||||
return Optional.of(new MediaTttChipController(
|
||||
context, windowManager, mainExecutor, backgroundExecutor));
|
||||
}
|
||||
|
||||
/** */
|
||||
@@ -92,10 +99,12 @@ public interface MediaModule {
|
||||
static Optional<MediaTttCommandLineHelper> providesMediaTttCommandLineHelper(
|
||||
MediaTttFlags mediaTttFlags,
|
||||
CommandRegistry commandRegistry,
|
||||
MediaTttChipController mediaTttChipController) {
|
||||
MediaTttChipController mediaTttChipController,
|
||||
@Main DelayableExecutor mainExecutor) {
|
||||
if (!mediaTttFlags.isMediaTttEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new MediaTttCommandLineHelper(commandRegistry, mediaTttChipController));
|
||||
return Optional.of(new MediaTttCommandLineHelper(
|
||||
commandRegistry, mediaTttChipController, mainExecutor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
const val TAG = "MediaTapToTransfer"
|
||||
@@ -41,6 +45,8 @@ const val TAG = "MediaTapToTransfer"
|
||||
class MediaTttChipController @Inject constructor(
|
||||
private val context: Context,
|
||||
private val windowManager: WindowManager,
|
||||
@Main private val mainExecutor: Executor,
|
||||
@Background private val backgroundExecutor: Executor,
|
||||
) {
|
||||
|
||||
@SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
|
||||
@@ -92,6 +98,11 @@ class MediaTttChipController @Inject constructor(
|
||||
}
|
||||
undoView.setOnClickListener(undoClickListener)
|
||||
|
||||
// Future handling
|
||||
if (chipState is TransferInitiated) {
|
||||
addFutureCallback(chipState)
|
||||
}
|
||||
|
||||
// Add view if necessary
|
||||
if (oldChipView == null) {
|
||||
windowManager.addView(chipView, windowLayoutParams)
|
||||
@@ -104,4 +115,29 @@ class MediaTttChipController @Inject constructor(
|
||||
windowManager.removeView(chipView)
|
||||
chipView = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the appropriate callbacks to [chipState.future] so that we update the chip correctly
|
||||
* when the future resolves.
|
||||
*/
|
||||
private fun addFutureCallback(chipState: TransferInitiated) {
|
||||
// Listen to the future on a background thread so we don't occupy the main thread while we
|
||||
// wait for it to complete.
|
||||
backgroundExecutor.execute {
|
||||
try {
|
||||
val undoRunnable = chipState.future.get(TRANSFER_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
// Make UI changes on the main thread
|
||||
mainExecutor.execute {
|
||||
displayChip(TransferSucceeded(chipState.otherDeviceName, undoRunnable))
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
// TODO(b/203800327): Maybe show a failure chip here if UX decides we need one.
|
||||
mainExecutor.execute {
|
||||
removeChip()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TRANSFER_TIMEOUT_SECONDS = 10L
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.systemui.media.taptotransfer
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.android.systemui.R
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* A class that stores all the information necessary to display the media tap-to-transfer chip in
|
||||
@@ -43,9 +44,15 @@ class MoveCloserToTransfer(
|
||||
|
||||
/**
|
||||
* A state representing that a transfer has been initiated (but not completed).
|
||||
*
|
||||
* @property future a future that will be resolved when the transfer has either succeeded or failed.
|
||||
* If the transfer succeeded, the future can optionally return an undo runnable (see
|
||||
* [TransferSucceeded.undoRunnable]). [MediaTttChipController] is responsible for transitioning
|
||||
* the chip to the [TransferSucceeded] state if the future resolves successfully.
|
||||
*/
|
||||
class TransferInitiated(
|
||||
otherDeviceName: String
|
||||
otherDeviceName: String,
|
||||
val future: Future<Runnable?>
|
||||
) : MediaTttChipState(R.string.media_transfer_playing, otherDeviceName)
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,12 @@ package com.android.systemui.media.taptotransfer
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.statusbar.commandline.Command
|
||||
import com.android.systemui.statusbar.commandline.CommandRegistry
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
import java.io.PrintWriter
|
||||
import java.util.concurrent.FutureTask
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -31,7 +34,8 @@ import javax.inject.Inject
|
||||
@SysUISingleton
|
||||
class MediaTttCommandLineHelper @Inject constructor(
|
||||
commandRegistry: CommandRegistry,
|
||||
private val mediaTttChipController: MediaTttChipController
|
||||
private val mediaTttChipController: MediaTttChipController,
|
||||
@Main private val mainExecutor: DelayableExecutor,
|
||||
) {
|
||||
init {
|
||||
commandRegistry.registerCommand(ADD_CHIP_COMMAND_TAG) { AddChipCommand() }
|
||||
@@ -46,7 +50,12 @@ class MediaTttCommandLineHelper @Inject constructor(
|
||||
mediaTttChipController.displayChip(MoveCloserToTransfer(otherDeviceName))
|
||||
}
|
||||
TRANSFER_INITIATED_COMMAND_NAME -> {
|
||||
mediaTttChipController.displayChip(TransferInitiated(otherDeviceName))
|
||||
val futureTask = FutureTask { fakeUndoRunnable }
|
||||
mediaTttChipController.displayChip(
|
||||
TransferInitiated(otherDeviceName, futureTask)
|
||||
)
|
||||
mainExecutor.executeDelayed({ futureTask.run() }, FUTURE_WAIT_TIME)
|
||||
|
||||
}
|
||||
TRANSFER_SUCCEEDED_COMMAND_NAME -> {
|
||||
mediaTttChipController.displayChip(
|
||||
@@ -94,3 +103,5 @@ val MOVE_CLOSER_TO_TRANSFER_COMMAND_NAME = MoveCloserToTransfer::class.simpleNam
|
||||
val TRANSFER_INITIATED_COMMAND_NAME = TransferInitiated::class.simpleName!!
|
||||
@VisibleForTesting
|
||||
val TRANSFER_SUCCEEDED_COMMAND_NAME = TransferSucceeded::class.simpleName!!
|
||||
|
||||
private const val FUTURE_WAIT_TIME = 2000L
|
||||
|
||||
@@ -23,8 +23,11 @@ import android.widget.TextView
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
@@ -37,6 +40,11 @@ import org.mockito.MockitoAnnotations
|
||||
@SmallTest
|
||||
class MediaTttChipControllerTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var fakeMainClock: FakeSystemClock
|
||||
private lateinit var fakeMainExecutor: FakeExecutor
|
||||
private lateinit var fakeBackgroundClock: FakeSystemClock
|
||||
private lateinit var fakeBackgroundExecutor: FakeExecutor
|
||||
|
||||
private lateinit var mediaTttChipController: MediaTttChipController
|
||||
|
||||
@Mock
|
||||
@@ -45,7 +53,13 @@ class MediaTttChipControllerTest : SysuiTestCase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
mediaTttChipController = MediaTttChipController(context, windowManager)
|
||||
fakeMainClock = FakeSystemClock()
|
||||
fakeMainExecutor = FakeExecutor(fakeMainClock)
|
||||
fakeBackgroundClock = FakeSystemClock()
|
||||
fakeBackgroundExecutor = FakeExecutor(fakeBackgroundClock)
|
||||
mediaTttChipController = MediaTttChipController(
|
||||
context, windowManager, fakeMainExecutor, fakeBackgroundExecutor
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,15 +107,78 @@ class MediaTttChipControllerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferInitiated_chipTextContainsDeviceName_loadingIcon_noUndo() {
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
|
||||
fun transferInitiated_futureNotResolvedYet_loadingIcon_noUndo() {
|
||||
val future: SettableFuture<Runnable?> = SettableFuture.create()
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, 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.getChipText()).contains(DEVICE_NAME)
|
||||
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
|
||||
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferInitiated_futureResolvedSuccessfully_switchesToTransferSucceeded() {
|
||||
val future: SettableFuture<Runnable?> = SettableFuture.create()
|
||||
val undoRunnable = Runnable { }
|
||||
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, future))
|
||||
|
||||
future.set(undoRunnable)
|
||||
fakeBackgroundExecutor.advanceClockToLast()
|
||||
fakeBackgroundExecutor.runAllReady()
|
||||
fakeMainExecutor.advanceClockToLast()
|
||||
val numRun = fakeMainExecutor.runAllReady()
|
||||
|
||||
// Assert we ran the future callback
|
||||
assertThat(numRun).isEqualTo(1)
|
||||
// Assert that we've moved to the successful state
|
||||
val chipView = getChipView()
|
||||
assertThat(chipView.getChipText()).contains(DEVICE_NAME)
|
||||
assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
|
||||
assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferInitiated_futureCancelled_chipRemoved() {
|
||||
val future: SettableFuture<Runnable?> = SettableFuture.create()
|
||||
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, future))
|
||||
|
||||
future.cancel(true)
|
||||
fakeBackgroundExecutor.advanceClockToLast()
|
||||
fakeBackgroundExecutor.runAllReady()
|
||||
fakeMainExecutor.advanceClockToLast()
|
||||
val numRun = fakeMainExecutor.runAllReady()
|
||||
|
||||
// Assert we ran the future callback
|
||||
assertThat(numRun).isEqualTo(1)
|
||||
// Assert that we've hidden the chip
|
||||
verify(windowManager).removeView(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferInitiated_futureNotResolvedAfterTimeout_chipRemoved() {
|
||||
val future: SettableFuture<Runnable?> = SettableFuture.create()
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, 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
|
||||
// out because we're waiting on the future indefinitely.
|
||||
fakeBackgroundExecutor.advanceClockToLast()
|
||||
fakeBackgroundExecutor.runAllReady()
|
||||
fakeMainExecutor.advanceClockToLast()
|
||||
val numRun = fakeMainExecutor.runAllReady()
|
||||
|
||||
// Assert we eventually decide to not wait for the future anymore
|
||||
assertThat(numRun).isEqualTo(1)
|
||||
// Assert we've hidden the chip
|
||||
verify(windowManager).removeView(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferSucceededNullUndoRunnable_chipTextContainsDeviceName_noLoadingIcon_noUndo() {
|
||||
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME, undoRunnable = null))
|
||||
@@ -137,14 +214,14 @@ class MediaTttChipControllerTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun changeFromCloserToTransferToTransferInitiated_loadingIconAppears() {
|
||||
mediaTttChipController.displayChip(MoveCloserToTransfer(DEVICE_NAME))
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, TEST_FUTURE))
|
||||
|
||||
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun changeFromTransferInitiatedToTransferSucceeded_loadingIconDisappears() {
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, TEST_FUTURE))
|
||||
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME))
|
||||
|
||||
assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
|
||||
@@ -152,7 +229,7 @@ class MediaTttChipControllerTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun changeFromTransferInitiatedToTransferSucceeded_undoButtonAppears() {
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME))
|
||||
mediaTttChipController.displayChip(TransferInitiated(DEVICE_NAME, TEST_FUTURE))
|
||||
mediaTttChipController.displayChip(TransferSucceeded(DEVICE_NAME) { })
|
||||
|
||||
assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
|
||||
@@ -182,3 +259,6 @@ class MediaTttChipControllerTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
private const val DEVICE_NAME = "My Tablet"
|
||||
// Use a settable future that hasn't yet been set so that we don't immediately switch to the success
|
||||
// state.
|
||||
private val TEST_FUTURE: SettableFuture<Runnable?> = SettableFuture.create()
|
||||
|
||||
@@ -20,7 +20,9 @@ import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.statusbar.commandline.Command
|
||||
import com.android.systemui.statusbar.commandline.CommandRegistry
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
@@ -46,7 +48,9 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() {
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
mediaTttCommandLineHelper =
|
||||
MediaTttCommandLineHelper(commandRegistry, mediaTttChipController)
|
||||
MediaTttCommandLineHelper(
|
||||
commandRegistry, mediaTttChipController, FakeExecutor(FakeSystemClock())
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
|
||||
Reference in New Issue
Block a user