Merge "Always show media recommendation on headphones connection." into sc-dev

This commit is contained in:
Cecilia Hong
2021-05-18 14:45:19 +00:00
committed by Android (Google) Code Review
9 changed files with 140 additions and 52 deletions

View File

@@ -208,9 +208,13 @@ class MediaCarouselController @Inject constructor(
}
}
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean
) {
Log.d(TAG, "My Smartspace media update is here")
addSmartspaceMediaRecommendations(key, data)
addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
MediaPlayerData.getMediaPlayer(key, null)?.let {
logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
it.mInstanceId,
@@ -327,7 +331,11 @@ class MediaCarouselController @Inject constructor(
return existingPlayer == null
}
private fun addSmartspaceMediaRecommendations(key: String, data: SmartspaceTarget) {
private fun addSmartspaceMediaRecommendations(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean
) {
Log.d(TAG, "Updating smartspace target in carousel")
if (MediaPlayerData.getMediaPlayer(key, null) != null) {
Log.w(TAG, "Skip adding smartspace target in carousel")
@@ -342,7 +350,7 @@ class MediaCarouselController @Inject constructor(
ViewGroup.LayoutParams.WRAP_CONTENT)
newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
newRecs.bindRecommendation(data, bgColor)
MediaPlayerData.addMediaRecommendation(key, newRecs)
MediaPlayerData.addMediaRecommendation(key, newRecs, shouldPrioritize)
updatePlayerToState(newRecs, noAnimation = true)
reorderAllPlayers()
updatePageIndicator()
@@ -671,17 +679,19 @@ class MediaCarouselController @Inject constructor(
internal object MediaPlayerData {
private val EMPTY = MediaData(-1, false, 0, null, null, null, null, null,
emptyList(), emptyList(), "INVALID", null, null, null, true, null)
// Whether should prioritize Smartspace card.
private var shouldPrioritizeSs: Boolean = false
data class MediaSortKey(
// Is Smartspace media recommendation. When the Smartspace media is present, it should
// always be the first card in carousel.
// Whether the item represents a Smartspace media recommendation.
val isSsMediaRec: Boolean,
val data: MediaData,
val updateTime: Long = 0
)
private val comparator =
compareByDescending<MediaSortKey> { it.isSsMediaRec }
compareByDescending<MediaSortKey>
{ if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
.thenByDescending { it.data.isPlaying }
.thenByDescending { it.data.isLocalSession }
.thenByDescending { !it.data.resumption }
@@ -697,7 +707,8 @@ internal object MediaPlayerData {
mediaPlayers.put(sortKey, player)
}
fun addMediaRecommendation(key: String, player: MediaControlPanel) {
fun addMediaRecommendation(key: String, player: MediaControlPanel, shouldPrioritize: Boolean) {
shouldPrioritizeSs = shouldPrioritize
removeMediaPlayer(key)
val sortKey = MediaSortKey(isSsMediaRec = true, EMPTY, System.currentTimeMillis())
mediaData.put(key, sortKey)

View File

@@ -38,7 +38,11 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
}
}
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean
) {
listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
}

View File

@@ -26,9 +26,11 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.CurrentUserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.time.SystemClock
import java.util.SortedMap
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.LinkedHashMap
private const val TAG = "MediaDataFilter"
private const val DEBUG = true
@@ -39,7 +41,7 @@ private const val DEBUG = true
*/
@VisibleForTesting
internal val SMARTSPACE_MAX_AGE = SystemProperties
.getLong("debug.sysui.smartspace_max_age", TimeUnit.HOURS.toMillis(3))
.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
/**
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
@@ -65,7 +67,8 @@ class MediaDataFilter @Inject constructor(
private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
// The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
private var hasSmartspace: Boolean = false
var hasSmartspace: Boolean = false
private set
private var reactivatedKey: String? = null
init {
@@ -99,39 +102,39 @@ class MediaDataFilter @Inject constructor(
}
}
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean
) {
var shouldPrioritizeMutable = shouldPrioritize
hasSmartspace = true
// Before forwarding the smartspace target, first check if we have recently inactive media
val now = systemClock.elapsedRealtime()
val sorted = userEntries.toSortedMap(compareBy {
userEntries.get(it)?.lastActive ?: -1
})
if (sorted.size > 0) {
val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
if (timeSinceActive < SMARTSPACE_MAX_AGE) {
val lastActiveKey = sorted.lastKey() // most recently active
val timeSinceActive = sorted.get(lastActiveKey)?.let {
now - it.lastActive
} ?: Long.MAX_VALUE
if (timeSinceActive < SMARTSPACE_MAX_AGE) {
// Notify listeners to consider this media active
Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
reactivatedKey = lastActiveKey
val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
listeners.forEach {
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData)
}
return
// Notify listeners to consider this media active
Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
reactivatedKey = lastActiveKey
val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
listeners.forEach {
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData)
}
} else {
// Mark to prioritize Smartspace card if no recent media.
shouldPrioritizeMutable = true
}
// If no recent media, continue with smartspace update
// Only proceed with the Smartspace update if the recommendation is not empty.
if (isMediaRecommendationEmpty(data)) {
Log.d(TAG, "Empty media recommendations. Skip showing the card")
return
}
// Proceed only if the Smartspace recommendation is not empty.
listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data) }
listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
}
override fun onMediaDataRemoved(key: String) {
@@ -158,7 +161,6 @@ class MediaDataFilter @Inject constructor(
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData)
}
}
return
}
listeners.forEach { it.onSmartspaceMediaDataRemoved(key) }
@@ -230,4 +232,26 @@ class MediaDataFilter @Inject constructor(
val mediaRecommendationList: List<SmartspaceAction> = data.getIconGrid()
return mediaRecommendationList == null || mediaRecommendationList.isEmpty()
}
/**
* Return the time since last active for the most-recent media.
*
* @param sortedEntries userEntries sorted from the earliest to the most-recent.
*
* @return The duration in milliseconds from the most-recent media's last active timestamp to
* the present. MAX_VALUE will be returned if there is no media.
*/
private fun timeSinceActiveForMostRecentMedia(
sortedEntries: SortedMap<String, MediaData>
): Long {
if (sortedEntries.isEmpty()) {
return Long.MAX_VALUE
}
val now = systemClock.elapsedRealtime()
val lastActiveKey = sortedEntries.lastKey() // most recently active
return sortedEntries.get(lastActiveKey)?.let {
now - it.lastActive
} ?: Long.MAX_VALUE
}
}

View File

@@ -817,8 +817,17 @@ class MediaDataManager(
*/
fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
/** Called whenever there's new Smartspace media data loaded. */
fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {}
/**
* Called whenever there's new Smartspace media data loaded.
*
* shouldPrioritize indicates the sorting priority of the Smartspace card. If true, it will
* be prioritized as the first card. Otherwise, it will show up as the last card as default.
*/
fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean = false
) {}
/**
* Called whenever a previously existing Media notification was removed

View File

@@ -56,7 +56,11 @@ class MediaHost constructor(
updateViewVisibility()
}
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean
) {
updateViewVisibility()
}

View File

@@ -135,7 +135,11 @@ class MediaSessionBasedFilter @Inject constructor(
}
}
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceTarget,
shouldPrioritize: Boolean
) {
backgroundExecutor.execute {
dispatchSmartspaceMediaDataLoaded(key, data)
}

View File

@@ -250,7 +250,8 @@ public class NotificationMediaManager implements Dumpable {
@Override
public void onSmartspaceMediaDataLoaded(@NonNull String key,
@NonNull SmartspaceTarget data) {
@NonNull SmartspaceTarget data, boolean shouldPrioritize) {
}
@Override
@@ -323,7 +324,8 @@ public class NotificationMediaManager implements Dumpable {
@Override
public void onSmartspaceMediaDataLoaded(@NonNull String key,
@NonNull SmartspaceTarget data) {
@NonNull SmartspaceTarget data, boolean shouldPrioritize) {
}
@Override

View File

@@ -30,6 +30,7 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito
@@ -228,37 +229,39 @@ class MediaDataFilterTest : SysuiTestCase() {
}
@Test
fun testOnSmartspaceMediaDataLoaded_noMedia_nonEmptyRecommendation_usesSmartspace() {
fun testOnSmartspaceMediaDataLoaded_noMedia_nonEmptyRec_prioritizesSmartspace() {
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData))
verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
}
@Test
fun testOnSmartspaceMediaDataLoaded_noMedia_emptyRecommendation_showsNothing() {
fun testOnSmartspaceMediaDataLoaded_noMedia_emptyRec_showsNothing() {
`when`(smartspaceData.iconGrid).thenReturn(listOf())
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener, never())
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData))
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), anyBoolean())
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
}
@Test
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_nonEmptyRecommendation_usesSmartspace() {
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_nonEmptyRec_prioritizesSmartspace() {
val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData))
verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
}
@Test
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_emptyRecommendation_showsNothing() {
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_emptyRec_showsNothing() {
`when`(smartspaceData.iconGrid).thenReturn(listOf())
val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -267,12 +270,14 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener, never())
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData))
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), anyBoolean())
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
}
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_usesMedia() {
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_emptyRec_usesMedia() {
`when`(smartspaceData.iconGrid).thenReturn(listOf())
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
@@ -285,19 +290,41 @@ class MediaDataFilterTest : SysuiTestCase() {
val dataCurrentAndActive = dataCurrent.copy(active = true)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive))
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
// Smartspace update shouldn't be propagated for the empty rec list.
verify(listener, never())
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), anyBoolean())
}
@Test
fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsMedia() {
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_nonEmptyRec_usesBoth() {
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent))
// AND we get a smartspace signal
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
// THEN we should tell listeners to treat the media as active instead
val dataCurrentAndActive = dataCurrent.copy(active = true)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive))
assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
// Smartspace update should also be propagated but not prioritized.
verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
}
@Test
fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() {
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
assertThat(mediaDataFilter.hasSmartspace).isFalse()
}
@Test
fun testOnSmartspaceMediaDataRemoved_usedMedia_clearsMedia() {
fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() {
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -305,6 +332,8 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrent))
verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
assertThat(mediaDataFilter.hasSmartspace).isFalse()
}
}

View File

@@ -28,6 +28,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
@@ -41,7 +42,6 @@ import org.mockito.Mockito.`when` as whenever
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
private const val KEY_NONE_MEDIA_SMARTSPACE = "NONE_MEDIA_SMARTSPACE_ID"
private const val PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI"
private const val SESSION_ARTIST = "artist"
@@ -364,13 +364,14 @@ class MediaDataManagerTest : SysuiTestCase() {
fun testOnSmartspaceMediaDataLoaded_hasNewMediaTarget_callsListener() {
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
verify(listener).onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE), eq(mediaSmartspaceTarget))
eq(KEY_MEDIA_SMARTSPACE), eq(mediaSmartspaceTarget), eq(false))
}
@Test
fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
smartspaceMediaDataProvider.onTargetsAvailable(listOf())
verify(listener, never()).onSmartspaceMediaDataLoaded(anyObject(), anyObject())
verify(listener, never())
.onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
}
@Test