Merge "[Media TTT] Handle multiple devices" into tm-qpr-dev am: 473235f30b

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20389911

Change-Id: I7cb56de24e21a12524a3cf50514ee5ddf4556be5
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Michael Mikhail
2022-11-11 23:34:03 +00:00
committed by Automerger Merge Worker
11 changed files with 291 additions and 36 deletions

View File

@@ -61,7 +61,7 @@ class MediaTttCommandLineHelper @Inject constructor(
@SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
as StatusBarManager
val routeInfo = MediaRoute2Info.Builder("id", args[0])
val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0])
.addFeature("feature")
val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false")
if (useAppIcon) {
@@ -107,7 +107,7 @@ class MediaTttCommandLineHelper @Inject constructor(
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " +
"<deviceName> <chipState> useAppIcon=[true|false]")
"<deviceName> <chipState> useAppIcon=[true|false] <id>")
}
}
@@ -127,8 +127,10 @@ class MediaTttCommandLineHelper @Inject constructor(
@SuppressLint("WrongConstant") // sysui is allowed to call STATUS_BAR_SERVICE
val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
as StatusBarManager
val routeInfo = MediaRoute2Info.Builder("id", "Test Name")
.addFeature("feature")
val routeInfo = MediaRoute2Info.Builder(
if (args.size >= 3) args[2] else "id",
"Test Name"
).addFeature("feature")
val useAppIcon = !(args.size >= 2 && args[1] == "useAppIcon=false")
if (useAppIcon) {
routeInfo.setClientPackageName(TEST_PACKAGE_NAME)
@@ -144,7 +146,7 @@ class MediaTttCommandLineHelper @Inject constructor(
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar $RECEIVER_COMMAND " +
"<chipState> useAppIcon=[true|false]")
"<chipState> useAppIcon=[true|false] <id>")
}
}

View File

@@ -121,18 +121,32 @@ class MediaTttChipControllerReceiver @Inject constructor(
uiEventLogger.logReceiverStateChange(chipState)
if (chipState == ChipStateReceiver.FAR_FROM_SENDER) {
removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
removeView(routeInfo.id, removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
return
}
if (appIcon == null) {
displayView(ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appName))
displayView(
ChipReceiverInfo(
routeInfo,
appIconDrawableOverride = null,
appName,
id = routeInfo.id,
)
)
return
}
appIcon.loadDrawableAsync(
context,
Icon.OnDrawableLoadedListener { drawable ->
displayView(ChipReceiverInfo(routeInfo, drawable, appName))
displayView(
ChipReceiverInfo(
routeInfo,
drawable,
appName,
id = routeInfo.id,
)
)
},
// Notify the listener on the main handler since the listener will update
// the UI.
@@ -234,4 +248,5 @@ data class ChipReceiverInfo(
val appNameOverride: CharSequence?,
override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
override val id: String,
) : TemporaryViewInfo()

View File

@@ -108,7 +108,7 @@ constructor(
}
displayedState = null
chipbarCoordinator.removeView(removalReason)
chipbarCoordinator.removeView(routeInfo.id, removalReason)
} else {
displayedState = chipState
chipbarCoordinator.displayView(
@@ -162,6 +162,7 @@ constructor(
windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
timeoutMs = chipStateSender.timeout,
id = routeInfo.id,
)
}

View File

@@ -93,6 +93,13 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
/** A string that keeps track of wakelock reason once it is acquired till it gets released */
private var wakeReasonAcquired: String? = null
/**
* A stack of pairs of device id and temporary view info. This is used when there may be
* multiple devices in range, and we want to always display the chip for the most recently
* active device.
*/
internal val activeViews: ArrayDeque<Pair<String, T>> = ArrayDeque()
/**
* Displays the view with the provided [newInfo].
*
@@ -102,6 +109,12 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
fun displayView(newInfo: T) {
val currentDisplayInfo = displayInfo
// Update our list of active devices by removing it if necessary, then adding back at the
// front of the list
val id = newInfo.id
val position = findAndRemoveFromActiveViewsList(id)
activeViews.addFirst(Pair(id, newInfo))
if (currentDisplayInfo != null &&
currentDisplayInfo.info.windowTitle == newInfo.windowTitle) {
// We're already displaying information in the correctly-titled window, so we just need
@@ -113,7 +126,10 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
// We're already displaying information but that information is under a different
// window title. So, we need to remove the old window with the old title and add a
// new window with the new title.
removeView(removalReason = "New info has new window title: ${newInfo.windowTitle}")
removeView(
id,
removalReason = "New info has new window title: ${newInfo.windowTitle}"
)
}
// At this point, we're guaranteed to no longer be displaying a view.
@@ -140,7 +156,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
}
wakeLock?.acquire(newInfo.wakeReason)
wakeReasonAcquired = newInfo.wakeReason
logger.logViewAddition(newInfo.windowTitle)
logger.logViewAddition(id, newInfo.windowTitle)
inflateAndUpdateView(newInfo)
}
@@ -151,9 +167,13 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
// include it just to be safe.
FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
)
cancelViewTimeout?.run()
// Only cancel timeout of the most recent view displayed, as it will be reset.
if (position == 0) {
cancelViewTimeout?.run()
}
cancelViewTimeout = mainExecutor.executeDelayed(
{ removeView(REMOVAL_REASON_TIMEOUT) },
{ removeView(id, REMOVAL_REASON_TIMEOUT) },
timeout.toLong()
)
}
@@ -196,28 +216,67 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
}
/**
* Hides the view.
* Hides the view given its [id].
*
* @param id the id of the device responsible of displaying the temp view.
* @param removalReason a short string describing why the view was removed (timeout, state
* change, etc.)
*/
fun removeView(removalReason: String) {
fun removeView(id: String, removalReason: String) {
val currentDisplayInfo = displayInfo ?: return
val removalPosition = findAndRemoveFromActiveViewsList(id)
if (removalPosition == null) {
logger.logViewRemovalIgnored(id, "view not found in the list")
return
}
if (removalPosition != 0) {
logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
return
}
logger.logViewRemoval(id, removalReason)
val newViewToDisplay = if (activeViews.isEmpty()) {
null
} else {
activeViews[0].second
}
val currentView = currentDisplayInfo.view
animateViewOut(currentView) {
windowManager.removeView(currentView)
wakeLock?.release(wakeReasonAcquired)
}
logger.logViewRemoval(removalReason)
configurationController.removeCallback(displayScaleListener)
// Re-set to null immediately (instead as part of the animation end runnable) so
// that if a new view event comes in while this view is animating out, we still display the
// new view appropriately.
// that if a new view event comes in while this view is animating out, we still display
// the new view appropriately.
displayInfo = null
// No need to time the view out since it's already gone
cancelViewTimeout?.run()
if (newViewToDisplay != null) {
mainExecutor.executeDelayed({ displayView(newViewToDisplay)}, DISPLAY_VIEW_DELAY)
}
}
/**
* Finds and removes the active view with the given [id] from the stack, or null if there is no
* active view with that ID
*
* @param id that temporary view belonged to.
*
* @return index of the view in the stack , otherwise null.
*/
private fun findAndRemoveFromActiveViewsList(id: String): Int? {
for (i in 0 until activeViews.size) {
if (activeViews[i].first == id) {
activeViews.removeAt(i)
return i
}
}
return null
}
/**
@@ -258,6 +317,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora
}
private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
const val DISPLAY_VIEW_DELAY = 50L
private data class IconInfo(
val iconName: String,

View File

@@ -37,6 +37,11 @@ abstract class TemporaryViewInfo {
* disappears.
*/
open val timeoutMs: Int = DEFAULT_TIMEOUT_MILLIS
/**
* The id of the temporary view.
*/
abstract val id: String
}
const val DEFAULT_TIMEOUT_MILLIS = 10000

View File

@@ -24,13 +24,42 @@ open class TemporaryViewLogger(
internal val buffer: LogBuffer,
internal val tag: String,
) {
/** Logs that we added the view in a window titled [windowTitle]. */
fun logViewAddition(windowTitle: String) {
buffer.log(tag, LogLevel.DEBUG, { str1 = windowTitle }, { "View added. window=$str1" })
/** Logs that we added the view with the given [id] in a window titled [windowTitle]. */
fun logViewAddition(id: String, windowTitle: String) {
buffer.log(
tag,
LogLevel.DEBUG,
{
str1 = windowTitle
str2 = id
},
{ "View added. window=$str1 id=$str2" }
)
}
/** Logs that we removed the chip for the given [reason]. */
fun logViewRemoval(reason: String) {
buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "View removed due to: $str1" })
/** Logs that we removed the view with the given [id] for the given [reason]. */
fun logViewRemoval(id: String, reason: String) {
buffer.log(
tag,
LogLevel.DEBUG,
{
str1 = reason
str2 = id
},
{ "View with id=$str2 is removed due to: $str1" }
)
}
/** Logs that we ignored removal of the view with the given [id]. */
fun logViewRemovalIgnored(id: String, reason: String) {
buffer.log(
tag,
LogLevel.DEBUG,
{
str1 = reason
str2 = id
},
{ "Removal of view with id=$str2 is ignored because $str1" }
)
}
}

View File

@@ -40,6 +40,7 @@ data class ChipbarInfo(
override val windowTitle: String,
override val wakeReason: String,
override val timeoutMs: Int,
override val id: String,
) : TemporaryViewInfo()
/** The possible items to display at the end of the chipbar. */

View File

@@ -261,7 +261,12 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
@Test
fun updateView_noOverrides_usesInfoFromAppIcon() {
controllerReceiver.displayView(
ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride = null)
ChipReceiverInfo(
routeInfo,
appIconDrawableOverride = null,
appNameOverride = null,
id = "id",
)
)
val view = getChipView()
@@ -274,7 +279,12 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
val drawableOverride = context.getDrawable(R.drawable.ic_celebration)!!
controllerReceiver.displayView(
ChipReceiverInfo(routeInfo, drawableOverride, appNameOverride = null)
ChipReceiverInfo(
routeInfo,
drawableOverride,
appNameOverride = null,
id = "id",
)
)
val view = getChipView()
@@ -286,7 +296,12 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
val appNameOverride = "Sweet New App"
controllerReceiver.displayView(
ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride)
ChipReceiverInfo(
routeInfo,
appIconDrawableOverride = null,
appNameOverride,
id = "id",
)
)
val view = getChipView()
@@ -340,7 +355,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() {
.addFeature("feature")
.setClientPackageName(packageName)
.build()
return ChipReceiverInfo(routeInfo, null, null)
return ChipReceiverInfo(routeInfo, null, null, id = "id")
}
private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)

View File

@@ -119,7 +119,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
)
)
verify(logger).logViewAddition("Fake Window Title")
verify(logger).logViewAddition("id", "Fake Window Title")
}
@Test
@@ -153,7 +153,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
underTest.displayView(getState())
assertThat(fakeWakeLock.isHeld).isTrue()
underTest.removeView("test reason")
underTest.removeView("id", "test reason")
assertThat(fakeWakeLock.isHeld).isFalse()
}
@@ -262,6 +262,127 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
}
@Test
fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
}
@Test
fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
underTest.displayView(ViewInfo("First name", id = "id1"))
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.removeView("id1", "test reason")
verify(windowManager, never()).removeView(any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
}
@Test
fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
underTest.displayView(ViewInfo("First name", id = "id1"))
underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.displayView(ViewInfo("Third name", id = "id3"))
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.removeView("id3", "test reason")
verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
reset(windowManager)
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
}
@Test
fun multipleViewsWithDifferentIds_oneViewStateChanged_stackHasRecentState() {
underTest.displayView(ViewInfo("First name", id = "id1"))
underTest.displayView(ViewInfo("New name", id = "id1"))
verify(windowManager).addView(any(), any())
reset(windowManager)
underTest.displayView(ViewInfo("Second name", id = "id2"))
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name")
assertThat(underTest.activeViews[0].second.name).isEqualTo("New name")
reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
}
@Test
fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() {
underTest.displayView(ViewInfo("First name", id = "id1"))
fakeClock.advanceTime(TIMEOUT_MS / 3)
underTest.displayView(ViewInfo("Second name", id = "id2"))
fakeClock.advanceTime(TIMEOUT_MS / 3)
underTest.displayView(ViewInfo("Third name", id = "id3"))
reset(windowManager)
fakeClock.advanceTime(TIMEOUT_MS + 1)
verify(windowManager).removeView(any())
verify(windowManager, never()).addView(any(), any())
assertThat(underTest.activeViews.size).isEqualTo(0)
}
@Test
fun removeView_viewRemovedAndRemovalLogged() {
// First, add the view
@@ -269,15 +390,16 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
// Then, remove it
val reason = "test reason"
underTest.removeView(reason)
val deviceId = "id"
underTest.removeView(deviceId, reason)
verify(windowManager).removeView(any())
verify(logger).logViewRemoval(reason)
verify(logger).logViewRemoval(deviceId, reason)
}
@Test
fun removeView_noAdd_viewNotRemoved() {
underTest.removeView("reason")
underTest.removeView("id", "reason")
verify(windowManager, never()).removeView(any())
}
@@ -329,7 +451,8 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() {
val name: String,
override val windowTitle: String = "Window Title",
override val wakeReason: String = "WAKE_REASON",
override val timeoutMs: Int = 1
override val timeoutMs: Int = 1,
override val id: String = "id",
) : TemporaryViewInfo()
}

View File

@@ -44,7 +44,7 @@ class TemporaryViewLoggerTest : SysuiTestCase() {
@Test
fun logViewAddition_bufferHasLog() {
logger.logViewAddition("Test Window Title")
logger.logViewAddition("test id", "Test Window Title")
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
@@ -57,7 +57,8 @@ class TemporaryViewLoggerTest : SysuiTestCase() {
@Test
fun logViewRemoval_bufferHasTagAndReason() {
val reason = "test reason"
logger.logViewRemoval(reason)
val deviceId = "test id"
logger.logViewRemoval(deviceId, reason)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
@@ -65,6 +66,7 @@ class TemporaryViewLoggerTest : SysuiTestCase() {
assertThat(actualString).contains(TAG)
assertThat(actualString).contains(reason)
assertThat(actualString).contains(deviceId)
}
}

View File

@@ -377,6 +377,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
windowTitle = WINDOW_TITLE,
wakeReason = WAKE_REASON,
timeoutMs = TIMEOUT,
id = DEVICE_ID,
)
}
@@ -401,3 +402,4 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
private const val TIMEOUT = 10000
private const val WINDOW_TITLE = "Test Chipbar Window Title"
private const val WAKE_REASON = "TEST_CHIPBAR_WAKE_REASON"
private const val DEVICE_ID = "id"