Format media control files

- Run ktfmt for Kotlin files that were moved, to follow current style guide
- Add missing copyright headers

Test: build
Bug: 241485754
Change-Id: Icc2a9246dfaea8352824a5a181b4c06d3bcb4a46
This commit is contained in:
Beth Thibodeau
2022-10-18 20:04:18 -05:00
parent 6e74a44107
commit 0d8858f204
59 changed files with 5515 additions and 4719 deletions

View File

@@ -189,45 +189,8 @@
-packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
-packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
-packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
-packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
-packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
-packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
-packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
-packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
-packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
-packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
-packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
-packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
-packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
-packages/SystemUI/src/com/android/systemui/media/MediaData.kt
-packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
-packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
-packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
-packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
-packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
-packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
-packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
-packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
-packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
-packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt -packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
-packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt -packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt
-packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
-packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
-packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
-packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
-packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
-packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
-packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
-packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
-packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
-packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
-packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
-packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
-packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
-packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
-packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
-packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
-packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
-packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt -packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
-packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
-packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -653,26 +616,6 @@
-packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt -packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt -packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
-packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt -packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt

View File

@@ -98,11 +98,6 @@ class GutsViewHolder constructor(itemView: View) {
} }
companion object { companion object {
val ids = setOf( val ids = setOf(R.id.remove_text, R.id.cancel, R.id.dismiss, R.id.settings)
R.id.remove_text,
R.id.cancel,
R.id.dismiss,
R.id.settings
)
} }
} }

View File

@@ -27,69 +27,42 @@ import com.android.systemui.R
data class MediaData( data class MediaData(
val userId: Int, val userId: Int,
val initialized: Boolean = false, val initialized: Boolean = false,
/** /** App name that will be displayed on the player. */
* App name that will be displayed on the player.
*/
val app: String?, val app: String?,
/** /** App icon shown on player. */
* App icon shown on player.
*/
val appIcon: Icon?, val appIcon: Icon?,
/** /** Artist name. */
* Artist name.
*/
val artist: CharSequence?, val artist: CharSequence?,
/** /** Song name. */
* Song name.
*/
val song: CharSequence?, val song: CharSequence?,
/** /** Album artwork. */
* Album artwork.
*/
val artwork: Icon?, val artwork: Icon?,
/** /** List of generic action buttons for the media player, based on notification actions */
* List of generic action buttons for the media player, based on notification actions
*/
val actions: List<MediaAction>, val actions: List<MediaAction>,
/** /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
* Same as above, but shown on smaller versions of the player, like in QQS or keyguard.
*/
val actionsToShowInCompact: List<Int>, val actionsToShowInCompact: List<Int>,
/** /**
* Semantic actions buttons, based on the PlaybackState of the media session. * Semantic actions buttons, based on the PlaybackState of the media session. If present, these
* If present, these actions will be preferred in the UI over [actions] * actions will be preferred in the UI over [actions]
*/ */
val semanticActions: MediaButton? = null, val semanticActions: MediaButton? = null,
/** /** Package name of the app that's posting the media. */
* Package name of the app that's posting the media.
*/
val packageName: String, val packageName: String,
/** /** Unique media session identifier. */
* Unique media session identifier.
*/
val token: MediaSession.Token?, val token: MediaSession.Token?,
/** /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */
* Action to perform when the player is tapped.
* This is unrelated to {@link #actions}.
*/
val clickIntent: PendingIntent?, val clickIntent: PendingIntent?,
/** /** Where the media is playing: phone, headphones, ear buds, remote session. */
* Where the media is playing: phone, headphones, ear buds, remote session.
*/
val device: MediaDeviceData?, val device: MediaDeviceData?,
/** /**
* When active, a player will be displayed on keyguard and quick-quick settings. * When active, a player will be displayed on keyguard and quick-quick settings. This is
* This is unrelated to the stream being playing or not, a player will not be active if * unrelated to the stream being playing or not, a player will not be active if timed out, or in
* timed out, or in resumption mode. * resumption mode.
*/ */
var active: Boolean, var active: Boolean,
/** /** Action that should be performed to restart a non active session. */
* Action that should be performed to restart a non active session.
*/
var resumeAction: Runnable?, var resumeAction: Runnable?,
/** /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */
* Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE
*/
var playbackLocation: Int = PLAYBACK_LOCAL, var playbackLocation: Int = PLAYBACK_LOCAL,
/** /**
* Indicates that this player is a resumption player (ie. It only shows a play actions which * Indicates that this player is a resumption player (ie. It only shows a play actions which
@@ -102,29 +75,19 @@ data class MediaData(
val notificationKey: String? = null, val notificationKey: String? = null,
var hasCheckedForResume: Boolean = false, var hasCheckedForResume: Boolean = false,
/** /** If apps do not report PlaybackState, set as null to imply 'undetermined' */
* If apps do not report PlaybackState, set as null to imply 'undetermined'
*/
val isPlaying: Boolean? = null, val isPlaying: Boolean? = null,
/** /** Set from the notification and used as fallback when PlaybackState cannot be determined */
* Set from the notification and used as fallback when PlaybackState cannot be determined
*/
val isClearable: Boolean = true, val isClearable: Boolean = true,
/** /** Timestamp when this player was last active. */
* Timestamp when this player was last active.
*/
var lastActive: Long = 0L, var lastActive: Long = 0L,
/** /** Instance ID for logging purposes */
* Instance ID for logging purposes
*/
val instanceId: InstanceId, val instanceId: InstanceId,
/** /** The UID of the app, used for logging */
* The UID of the app, used for logging
*/
val appUid: Int val appUid: Int
) { ) {
companion object { companion object {
@@ -141,37 +104,21 @@ data class MediaData(
} }
} }
/** /** Contains [MediaAction] objects which represent specific buttons in the UI */
* Contains [MediaAction] objects which represent specific buttons in the UI
*/
data class MediaButton( data class MediaButton(
/** /** Play/pause button */
* Play/pause button
*/
val playOrPause: MediaAction? = null, val playOrPause: MediaAction? = null,
/** /** Next button, or custom action */
* Next button, or custom action
*/
val nextOrCustom: MediaAction? = null, val nextOrCustom: MediaAction? = null,
/** /** Previous button, or custom action */
* Previous button, or custom action
*/
val prevOrCustom: MediaAction? = null, val prevOrCustom: MediaAction? = null,
/** /** First custom action space */
* First custom action space
*/
val custom0: MediaAction? = null, val custom0: MediaAction? = null,
/** /** Second custom action space */
* Second custom action space
*/
val custom1: MediaAction? = null, val custom1: MediaAction? = null,
/** /** Whether to reserve the empty space when the nextOrCustom is null */
* Whether to reserve the empty space when the nextOrCustom is null
*/
val reserveNext: Boolean = false, val reserveNext: Boolean = false,
/** /** Whether to reserve the empty space when the prevOrCustom is null */
* Whether to reserve the empty space when the prevOrCustom is null
*/
val reservePrev: Boolean = false val reservePrev: Boolean = false
) { ) {
fun getActionById(id: Int): MediaAction? { fun getActionById(id: Int): MediaAction? {
@@ -201,7 +148,8 @@ data class MediaAction(
/** State of the media device. */ /** State of the media device. */
data class MediaDeviceData data class MediaDeviceData
@JvmOverloads constructor( @JvmOverloads
constructor(
/** Whether or not to enable the chip */ /** Whether or not to enable the chip */
val enabled: Boolean, val enabled: Boolean,
@@ -221,8 +169,8 @@ data class MediaDeviceData
val showBroadcastButton: Boolean val showBroadcastButton: Boolean
) { ) {
/** /**
* Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is
* is ignored because it can change by reference frequently depending on the device type's * ignored because it can change by reference frequently depending on the device type's
* implementation, but this is not usually relevant unless other info has changed * implementation, but this is not usually relevant unless other info has changed
*/ */
fun equalsWithoutIcon(other: MediaDeviceData?): Boolean { fun equalsWithoutIcon(other: MediaDeviceData?): Boolean {

View File

@@ -30,9 +30,7 @@ import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "MediaViewHolder" private const val TAG = "MediaViewHolder"
/** /** Holder class for media player view */
* Holder class for media player view
*/
class MediaViewHolder constructor(itemView: View) { class MediaViewHolder constructor(itemView: View) {
val player = itemView as TransitionLayout val player = itemView as TransitionLayout
@@ -53,8 +51,7 @@ class MediaViewHolder constructor(itemView: View) {
// These views are only shown while the user is actively scrubbing // These views are only shown while the user is actively scrubbing
val scrubbingElapsedTimeView: TextView = val scrubbingElapsedTimeView: TextView =
itemView.requireViewById(R.id.media_scrubbing_elapsed_time) itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
val scrubbingTotalTimeView: TextView = val scrubbingTotalTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_total_time)
itemView.requireViewById(R.id.media_scrubbing_total_time)
val gutsViewHolder = GutsViewHolder(itemView) val gutsViewHolder = GutsViewHolder(itemView)
@@ -87,15 +84,7 @@ class MediaViewHolder constructor(itemView: View) {
} }
fun getTransparentActionButtons(): List<ImageButton> { fun getTransparentActionButtons(): List<ImageButton> {
return listOf( return listOf(actionNext, actionPrev, action0, action1, action2, action3, action4)
actionNext,
actionPrev,
action0,
action1,
action2,
action3,
action4
)
} }
fun marquee(start: Boolean, delay: Long) { fun marquee(start: Boolean, delay: Long) {
@@ -109,10 +98,8 @@ class MediaViewHolder constructor(itemView: View) {
* @param inflater LayoutInflater to use to inflate the layout. * @param inflater LayoutInflater to use to inflate the layout.
* @param parent Parent of inflated view. * @param parent Parent of inflated view.
*/ */
@JvmStatic fun create( @JvmStatic
inflater: LayoutInflater, fun create(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder {
parent: ViewGroup
): MediaViewHolder {
val mediaView = inflater.inflate(R.layout.media_session_view, parent, false) val mediaView = inflater.inflate(R.layout.media_session_view, parent, false)
mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null) mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// Because this media view (a TransitionLayout) is used to measure and layout the views // Because this media view (a TransitionLayout) is used to measure and layout the views
@@ -125,7 +112,8 @@ class MediaViewHolder constructor(itemView: View) {
} }
} }
val controlsIds = setOf( val controlsIds =
setOf(
R.id.icon, R.id.icon,
R.id.app_name, R.id.app_name,
R.id.header_title, R.id.header_title,
@@ -143,27 +131,23 @@ class MediaViewHolder constructor(itemView: View) {
R.id.icon, R.id.icon,
R.id.media_scrubbing_elapsed_time, R.id.media_scrubbing_elapsed_time,
R.id.media_scrubbing_total_time R.id.media_scrubbing_total_time
) )
// Buttons used for notification-based actions // Buttons used for notification-based actions
val genericButtonIds = setOf( val genericButtonIds =
R.id.action0, setOf(R.id.action0, R.id.action1, R.id.action2, R.id.action3, R.id.action4)
R.id.action1,
R.id.action2,
R.id.action3,
R.id.action4
)
val expandedBottomActionIds = setOf( val expandedBottomActionIds =
R.id.actionPrev, setOf(
R.id.actionNext, R.id.actionPrev,
R.id.action0, R.id.actionNext,
R.id.action1, R.id.action0,
R.id.action2, R.id.action1,
R.id.action3, R.id.action2,
R.id.action4, R.id.action3,
R.id.media_scrubbing_elapsed_time, R.id.action4,
R.id.media_scrubbing_total_time R.id.media_scrubbing_elapsed_time,
) R.id.media_scrubbing_total_time
)
} }
} }

View File

@@ -31,34 +31,49 @@ import com.android.systemui.media.controls.ui.SquigglyProgress
* *
* <p>Updates the seek bar views in response to changes to the model. * <p>Updates the seek bar views in response to changes to the model.
*/ */
open class SeekBarObserver( open class SeekBarObserver(private val holder: MediaViewHolder) :
private val holder: MediaViewHolder Observer<SeekBarViewModel.Progress> {
) : Observer<SeekBarViewModel.Progress> {
companion object { companion object {
@JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750 @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750
@JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250 @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
} }
val seekBarEnabledMaxHeight = holder.seekBar.context.resources val seekBarEnabledMaxHeight =
.getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height) holder.seekBar.context.resources.getDimensionPixelSize(
val seekBarDisabledHeight = holder.seekBar.context.resources R.dimen.qs_media_enabled_seekbar_height
.getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height) )
val seekBarEnabledVerticalPadding = holder.seekBar.context.resources val seekBarDisabledHeight =
.getDimensionPixelSize(R.dimen.qs_media_session_enabled_seekbar_vertical_padding) holder.seekBar.context.resources.getDimensionPixelSize(
val seekBarDisabledVerticalPadding = holder.seekBar.context.resources R.dimen.qs_media_disabled_seekbar_height
.getDimensionPixelSize(R.dimen.qs_media_session_disabled_seekbar_vertical_padding) )
val seekBarEnabledVerticalPadding =
holder.seekBar.context.resources.getDimensionPixelSize(
R.dimen.qs_media_session_enabled_seekbar_vertical_padding
)
val seekBarDisabledVerticalPadding =
holder.seekBar.context.resources.getDimensionPixelSize(
R.dimen.qs_media_session_disabled_seekbar_vertical_padding
)
var seekBarResetAnimator: Animator? = null var seekBarResetAnimator: Animator? = null
init { init {
val seekBarProgressWavelength = holder.seekBar.context.resources val seekBarProgressWavelength =
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength).toFloat() holder.seekBar.context.resources
val seekBarProgressAmplitude = holder.seekBar.context.resources .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength)
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude).toFloat() .toFloat()
val seekBarProgressPhase = holder.seekBar.context.resources val seekBarProgressAmplitude =
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase).toFloat() holder.seekBar.context.resources
val seekBarProgressStrokeWidth = holder.seekBar.context.resources .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude)
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width).toFloat() .toFloat()
val seekBarProgressPhase =
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase)
.toFloat()
val seekBarProgressStrokeWidth =
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width)
.toFloat()
val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
progressDrawable?.let { progressDrawable?.let {
it.waveLength = seekBarProgressWavelength it.waveLength = seekBarProgressWavelength
@@ -98,16 +113,18 @@ open class SeekBarObserver(
} }
holder.seekBar.setMax(data.duration) holder.seekBar.setMax(data.duration)
val totalTimeString = DateUtils.formatElapsedTime( val totalTimeString =
data.duration / DateUtils.SECOND_IN_MILLIS) DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) { if (data.scrubbing) {
holder.scrubbingTotalTimeView.text = totalTimeString holder.scrubbingTotalTimeView.text = totalTimeString
} }
data.elapsedTime?.let { data.elapsedTime?.let {
if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) { if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) {
if (it <= RESET_ANIMATION_THRESHOLD_MS && if (
holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS) { it <= RESET_ANIMATION_THRESHOLD_MS &&
holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS
) {
// This animation resets for every additional update to zero. // This animation resets for every additional update to zero.
val animator = buildResetAnimator(it) val animator = buildResetAnimator(it)
animator.start() animator.start()
@@ -117,24 +134,29 @@ open class SeekBarObserver(
} }
} }
val elapsedTimeString = DateUtils.formatElapsedTime( val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
it / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) { if (data.scrubbing) {
holder.scrubbingElapsedTimeView.text = elapsedTimeString holder.scrubbingElapsedTimeView.text = elapsedTimeString
} }
holder.seekBar.contentDescription = holder.seekBar.context.getString( holder.seekBar.contentDescription =
R.string.controls_media_seekbar_description, holder.seekBar.context.getString(
elapsedTimeString, R.string.controls_media_seekbar_description,
totalTimeString elapsedTimeString,
) totalTimeString
)
} }
} }
@VisibleForTesting @VisibleForTesting
open fun buildResetAnimator(targetTime: Int): Animator { open fun buildResetAnimator(targetTime: Int): Animator {
val animator = ObjectAnimator.ofInt(holder.seekBar, "progress", val animator =
holder.seekBar.progress, targetTime + RESET_ANIMATION_DURATION_MS) ObjectAnimator.ofInt(
holder.seekBar,
"progress",
holder.seekBar.progress,
targetTime + RESET_ANIMATION_DURATION_MS
)
animator.setAutoCancel(true) animator.setAutoCancel(true)
animator.duration = RESET_ANIMATION_DURATION_MS.toLong() animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
animator.interpolator = Interpolators.EMPHASIZED animator.interpolator = Interpolators.EMPHASIZED

View File

@@ -42,8 +42,8 @@ private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10
private fun PlaybackState.isInMotion(): Boolean { private fun PlaybackState.isInMotion(): Boolean {
return this.state == PlaybackState.STATE_PLAYING || return this.state == PlaybackState.STATE_PLAYING ||
this.state == PlaybackState.STATE_FAST_FORWARDING || this.state == PlaybackState.STATE_FAST_FORWARDING ||
this.state == PlaybackState.STATE_REWINDING this.state == PlaybackState.STATE_REWINDING
} }
/** /**
@@ -59,8 +59,8 @@ private fun PlaybackState.computePosition(duration: Long): Long {
val updateTime = this.getLastPositionUpdateTime() val updateTime = this.getLastPositionUpdateTime()
val currentTime = SystemClock.elapsedRealtime() val currentTime = SystemClock.elapsedRealtime()
if (updateTime > 0) { if (updateTime > 0) {
var position = (this.playbackSpeed * (currentTime - updateTime)).toLong() + var position =
this.getPosition() (this.playbackSpeed * (currentTime - updateTime)).toLong() + this.getPosition()
if (duration >= 0 && position > duration) { if (duration >= 0 && position > duration) {
position = duration.toLong() position = duration.toLong()
} else if (position < 0) { } else if (position < 0) {
@@ -73,7 +73,9 @@ private fun PlaybackState.computePosition(duration: Long): Long {
} }
/** ViewModel for seek bar in QS media player. */ /** ViewModel for seek bar in QS media player. */
class SeekBarViewModel @Inject constructor( class SeekBarViewModel
@Inject
constructor(
@Background private val bgExecutor: RepeatableExecutor, @Background private val bgExecutor: RepeatableExecutor,
private val falsingManager: FalsingManager, private val falsingManager: FalsingManager,
) { ) {
@@ -86,9 +88,7 @@ class SeekBarViewModel @Inject constructor(
} }
_progress.postValue(value) _progress.postValue(value)
} }
private val _progress = MutableLiveData<Progress>().apply { private val _progress = MutableLiveData<Progress>().apply { postValue(_data) }
postValue(_data)
}
val progress: LiveData<Progress> val progress: LiveData<Progress>
get() = _progress get() = _progress
private var controller: MediaController? = null private var controller: MediaController? = null
@@ -100,20 +100,21 @@ class SeekBarViewModel @Inject constructor(
} }
} }
private var playbackState: PlaybackState? = null private var playbackState: PlaybackState? = null
private var callback = object : MediaController.Callback() { private var callback =
override fun onPlaybackStateChanged(state: PlaybackState?) { object : MediaController.Callback() {
playbackState = state override fun onPlaybackStateChanged(state: PlaybackState?) {
if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) { playbackState = state
if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) {
clearController()
} else {
checkIfPollingNeeded()
}
}
override fun onSessionDestroyed() {
clearController() clearController()
} else {
checkIfPollingNeeded()
} }
} }
override fun onSessionDestroyed() {
clearController()
}
}
private var cancel: Runnable? = null private var cancel: Runnable? = null
/** Indicates if the seek interaction is considered a false guesture. */ /** Indicates if the seek interaction is considered a false guesture. */
@@ -121,12 +122,13 @@ class SeekBarViewModel @Inject constructor(
/** Listening state (QS open or closed) is used to control polling of progress. */ /** Listening state (QS open or closed) is used to control polling of progress. */
var listening = true var listening = true
set(value) = bgExecutor.execute { set(value) =
if (field != value) { bgExecutor.execute {
field = value if (field != value) {
checkIfPollingNeeded() field = value
checkIfPollingNeeded()
}
} }
}
private var scrubbingChangeListener: ScrubbingChangeListener? = null private var scrubbingChangeListener: ScrubbingChangeListener? = null
private var enabledChangeListener: EnabledChangeListener? = null private var enabledChangeListener: EnabledChangeListener? = null
@@ -144,14 +146,13 @@ class SeekBarViewModel @Inject constructor(
lateinit var logSeek: () -> Unit lateinit var logSeek: () -> Unit
/** /** Event indicating that the user has started interacting with the seek bar. */
* Event indicating that the user has started interacting with the seek bar.
*/
@AnyThread @AnyThread
fun onSeekStarting() = bgExecutor.execute { fun onSeekStarting() =
scrubbing = true bgExecutor.execute {
isFalseSeek = false scrubbing = true
} isFalseSeek = false
}
/** /**
* Event indicating that the user has moved the seek bar. * Event indicating that the user has moved the seek bar.
@@ -159,47 +160,51 @@ class SeekBarViewModel @Inject constructor(
* @param position Current location in the track. * @param position Current location in the track.
*/ */
@AnyThread @AnyThread
fun onSeekProgress(position: Long) = bgExecutor.execute { fun onSeekProgress(position: Long) =
if (scrubbing) { bgExecutor.execute {
// The user hasn't yet finished their touch gesture, so only update the data for visual if (scrubbing) {
// feedback and don't update [controller] yet. // The user hasn't yet finished their touch gesture, so only update the data for
_data = _data.copy(elapsedTime = position.toInt()) // visual
} else { // feedback and don't update [controller] yet.
// The seek progress came from an a11y action and we should immediately update to the _data = _data.copy(elapsedTime = position.toInt())
// new position. (a11y actions to change the seekbar position don't trigger } else {
// SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.) // The seek progress came from an a11y action and we should immediately update to
onSeek(position) // the
// new position. (a11y actions to change the seekbar position don't trigger
// SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.)
onSeek(position)
}
} }
}
/** /** Event indicating that the seek interaction is a false gesture and it should be ignored. */
* Event indicating that the seek interaction is a false gesture and it should be ignored.
*/
@AnyThread @AnyThread
fun onSeekFalse() = bgExecutor.execute { fun onSeekFalse() =
if (scrubbing) { bgExecutor.execute {
isFalseSeek = true if (scrubbing) {
isFalseSeek = true
}
} }
}
/** /**
* Handle request to change the current position in the media track. * Handle request to change the current position in the media track.
* @param position Place to seek to in the track. * @param position Place to seek to in the track.
*/ */
@AnyThread @AnyThread
fun onSeek(position: Long) = bgExecutor.execute { fun onSeek(position: Long) =
if (isFalseSeek) { bgExecutor.execute {
scrubbing = false if (isFalseSeek) {
checkPlaybackPosition() scrubbing = false
} else { checkPlaybackPosition()
logSeek() } else {
controller?.transportControls?.seekTo(position) logSeek()
// Invalidate the cached playbackState to avoid the thumb jumping back to the previous controller?.transportControls?.seekTo(position)
// position. // Invalidate the cached playbackState to avoid the thumb jumping back to the
playbackState = null // previous
scrubbing = false // position.
playbackState = null
scrubbing = false
}
} }
}
/** /**
* Updates media information. * Updates media information.
@@ -216,11 +221,18 @@ class SeekBarViewModel @Inject constructor(
val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
val position = playbackState?.position?.toInt() val position = playbackState?.position?.toInt()
val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0 val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0
val playing = NotificationMediaManager val playing =
.isPlayingState(playbackState?.state ?: PlaybackState.STATE_NONE) NotificationMediaManager.isPlayingState(
val enabled = if (playbackState == null || playbackState?.state ?: PlaybackState.STATE_NONE
playbackState?.getState() == PlaybackState.STATE_NONE || )
(duration <= 0)) false else true val enabled =
if (
playbackState == null ||
playbackState?.getState() == PlaybackState.STATE_NONE ||
(duration <= 0)
)
false
else true
_data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration) _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration)
checkIfPollingNeeded() checkIfPollingNeeded()
} }
@@ -231,26 +243,26 @@ class SeekBarViewModel @Inject constructor(
* This should be called when the media session behind the controller has been destroyed. * This should be called when the media session behind the controller has been destroyed.
*/ */
@AnyThread @AnyThread
fun clearController() = bgExecutor.execute { fun clearController() =
controller = null bgExecutor.execute {
playbackState = null controller = null
cancel?.run() playbackState = null
cancel = null cancel?.run()
_data = _data.copy(enabled = false) cancel = null
} _data = _data.copy(enabled = false)
}
/** /** Call to clean up any resources. */
* Call to clean up any resources.
*/
@AnyThread @AnyThread
fun onDestroy() = bgExecutor.execute { fun onDestroy() =
controller = null bgExecutor.execute {
playbackState = null controller = null
cancel?.run() playbackState = null
cancel = null cancel?.run()
scrubbingChangeListener = null cancel = null
enabledChangeListener = null scrubbingChangeListener = null
} enabledChangeListener = null
}
@WorkerThread @WorkerThread
private fun checkPlaybackPosition() { private fun checkPlaybackPosition() {
@@ -266,8 +278,12 @@ class SeekBarViewModel @Inject constructor(
val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false
if (needed) { if (needed) {
if (cancel == null) { if (cancel == null) {
cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L, cancel =
POSITION_UPDATE_INTERVAL_MILLIS) bgExecutor.executeRepeatedly(
this::checkPlaybackPosition,
0L,
POSITION_UPDATE_INTERVAL_MILLIS
)
} }
} else { } else {
cancel?.run() cancel?.run()
@@ -353,9 +369,10 @@ class SeekBarViewModel @Inject constructor(
// Gesture detector helps decide which touch events to intercept. // Gesture detector helps decide which touch events to intercept.
private val detector = GestureDetectorCompat(bar.context, this) private val detector = GestureDetectorCompat(bar.context, this)
// Velocity threshold used to decide when a fling is considered a false gesture. // Velocity threshold used to decide when a fling is considered a false gesture.
private val flingVelocity: Int = ViewConfiguration.get(bar.context).run { private val flingVelocity: Int =
getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR ViewConfiguration.get(bar.context).run {
} getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR
}
// Indicates if the gesture should go to the seek bar or if it should be intercepted. // Indicates if the gesture should go to the seek bar or if it should be intercepted.
private var shouldGoToSeekBar = false private var shouldGoToSeekBar = false
@@ -385,9 +402,9 @@ class SeekBarViewModel @Inject constructor(
/** /**
* Handle down events that press down on the thumb. * Handle down events that press down on the thumb.
* *
* On the down action, determine a target box around the thumb to know when a scroll * On the down action, determine a target box around the thumb to know when a scroll gesture
* gesture starts by clicking on the thumb. The target box will be used by subsequent * starts by clicking on the thumb. The target box will be used by subsequent onScroll
* onScroll events. * events.
* *
* Returns true when the down event hits within the target box of the thumb. * Returns true when the down event hits within the target box of the thumb.
*/ */
@@ -398,17 +415,19 @@ class SeekBarViewModel @Inject constructor(
// TODO: account for thumb offset // TODO: account for thumb offset
val progress = bar.getProgress() val progress = bar.getProgress()
val range = bar.max - bar.min val range = bar.max - bar.min
val widthFraction = if (range > 0) { val widthFraction =
(progress - bar.min).toDouble() / range if (range > 0) {
} else { (progress - bar.min).toDouble() / range
0.0 } else {
} 0.0
}
val availableWidth = bar.width - padL - padR val availableWidth = bar.width - padL - padR
val thumbX = if (bar.isLayoutRtl()) { val thumbX =
padL + availableWidth * (1 - widthFraction) if (bar.isLayoutRtl()) {
} else { padL + availableWidth * (1 - widthFraction)
padL + availableWidth * widthFraction } else {
} padL + availableWidth * widthFraction
}
// Set the min, max boundaries of the thumb box. // Set the min, max boundaries of the thumb box.
// I'm cheating by using the height of the seek bar as the width of the box. // I'm cheating by using the height of the seek bar as the width of the box.
val halfHeight: Int = bar.height / 2 val halfHeight: Int = bar.height / 2

View File

@@ -35,26 +35,30 @@ class RecommendationViewHolder private constructor(itemView: View) {
// Recommendation screen // Recommendation screen
val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon) val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
val mediaCoverItems = listOf<ImageView>( val mediaCoverItems =
itemView.requireViewById(R.id.media_cover1), listOf<ImageView>(
itemView.requireViewById(R.id.media_cover2), itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover3) itemView.requireViewById(R.id.media_cover2),
) itemView.requireViewById(R.id.media_cover3)
val mediaCoverContainers = listOf<ViewGroup>( )
itemView.requireViewById(R.id.media_cover1_container), val mediaCoverContainers =
itemView.requireViewById(R.id.media_cover2_container), listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover3_container) itemView.requireViewById(R.id.media_cover1_container),
) itemView.requireViewById(R.id.media_cover2_container),
val mediaTitles: List<TextView> = listOf( itemView.requireViewById(R.id.media_cover3_container)
itemView.requireViewById(R.id.media_title1), )
itemView.requireViewById(R.id.media_title2), val mediaTitles: List<TextView> =
itemView.requireViewById(R.id.media_title3) listOf(
) itemView.requireViewById(R.id.media_title1),
val mediaSubtitles: List<TextView> = listOf( itemView.requireViewById(R.id.media_title2),
itemView.requireViewById(R.id.media_subtitle1), itemView.requireViewById(R.id.media_title3)
itemView.requireViewById(R.id.media_subtitle2), )
itemView.requireViewById(R.id.media_subtitle3) val mediaSubtitles: List<TextView> =
) listOf(
itemView.requireViewById(R.id.media_subtitle1),
itemView.requireViewById(R.id.media_subtitle2),
itemView.requireViewById(R.id.media_subtitle3)
)
val gutsViewHolder = GutsViewHolder(itemView) val gutsViewHolder = GutsViewHolder(itemView)
@@ -78,13 +82,14 @@ class RecommendationViewHolder private constructor(itemView: View) {
* @param inflater LayoutInflater to use to inflate the layout. * @param inflater LayoutInflater to use to inflate the layout.
* @param parent Parent of inflated view. * @param parent Parent of inflated view.
*/ */
@JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): @JvmStatic
RecommendationViewHolder { fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
val itemView = val itemView =
inflater.inflate( inflater.inflate(
R.layout.media_smartspace_recommendations, R.layout.media_smartspace_recommendations,
parent, parent,
false /* attachToRoot */) false /* attachToRoot */
)
// Because this media view (a TransitionLayout) is used to measure and layout the views // Because this media view (a TransitionLayout) is used to measure and layout the views
// in various states before being attached to its parent, we can't depend on the default // in various states before being attached to its parent, we can't depend on the default
// LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
@@ -93,35 +98,38 @@ class RecommendationViewHolder private constructor(itemView: View) {
} }
// Res Ids for the control components on the recommendation view. // Res Ids for the control components on the recommendation view.
val controlsIds = setOf( val controlsIds =
R.id.recommendation_card_icon, setOf(
R.id.media_cover1, R.id.recommendation_card_icon,
R.id.media_cover2, R.id.media_cover1,
R.id.media_cover3, R.id.media_cover2,
R.id.media_cover1_container, R.id.media_cover3,
R.id.media_cover2_container, R.id.media_cover1_container,
R.id.media_cover3_container, R.id.media_cover2_container,
R.id.media_title1, R.id.media_cover3_container,
R.id.media_title2, R.id.media_title1,
R.id.media_title3, R.id.media_title2,
R.id.media_subtitle1, R.id.media_title3,
R.id.media_subtitle2, R.id.media_subtitle1,
R.id.media_subtitle3 R.id.media_subtitle2,
) R.id.media_subtitle3
)
val mediaTitlesAndSubtitlesIds = setOf( val mediaTitlesAndSubtitlesIds =
R.id.media_title1, setOf(
R.id.media_title2, R.id.media_title1,
R.id.media_title3, R.id.media_title2,
R.id.media_subtitle1, R.id.media_title3,
R.id.media_subtitle2, R.id.media_subtitle1,
R.id.media_subtitle3 R.id.media_subtitle2,
) R.id.media_subtitle3
)
val mediaContainersIds = setOf( val mediaContainersIds =
R.id.media_cover1_container, setOf(
R.id.media_cover2_container, R.id.media_cover1_container,
R.id.media_cover3_container R.id.media_cover2_container,
) R.id.media_cover3_container
)
} }
} }

View File

@@ -25,55 +25,38 @@ import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceId
@VisibleForTesting @VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
/** State of a Smartspace media recommendations view. */ /** State of a Smartspace media recommendations view. */
data class SmartspaceMediaData( data class SmartspaceMediaData(
/** /** Unique id of a Smartspace media target. */
* Unique id of a Smartspace media target.
*/
val targetId: String, val targetId: String,
/** /** Indicates if the status is active. */
* Indicates if the status is active.
*/
val isActive: Boolean, val isActive: Boolean,
/** /** Package name of the media recommendations' provider-app. */
* Package name of the media recommendations' provider-app.
*/
val packageName: String, val packageName: String,
/** /** Action to perform when the card is tapped. Also contains the target's extra info. */
* Action to perform when the card is tapped. Also contains the target's extra info.
*/
val cardAction: SmartspaceAction?, val cardAction: SmartspaceAction?,
/** /** List of media recommendations. */
* List of media recommendations.
*/
val recommendations: List<SmartspaceAction>, val recommendations: List<SmartspaceAction>,
/** /** Intent for the user's initiated dismissal. */
* Intent for the user's initiated dismissal.
*/
val dismissIntent: Intent?, val dismissIntent: Intent?,
/** /** The timestamp in milliseconds that headphone is connected. */
* The timestamp in milliseconds that headphone is connected.
*/
val headphoneConnectionTimeMillis: Long, val headphoneConnectionTimeMillis: Long,
/** /** Instance ID for [MediaUiEventLogger] */
* Instance ID for [MediaUiEventLogger]
*/
val instanceId: InstanceId val instanceId: InstanceId
) { ) {
/** /**
* Indicates if all the data is valid. * Indicates if all the data is valid.
* *
* TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than
* ```
* [NUM_REQUIRED_RECOMMENDATIONS]. * [NUM_REQUIRED_RECOMMENDATIONS].
* ```
*/ */
fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS
/** /** Returns the list of [recommendations] that have valid data. */
* Returns the list of [recommendations] that have valid data.
*/
fun getValidRecommendations() = recommendations.filter { it.icon != null } fun getValidRecommendations() = recommendations.filter { it.icon != null }
/** Returns the upstream app name if available. */ /** Returns the upstream app name if available. */
@@ -92,7 +75,8 @@ data class SmartspaceMediaData(
Log.w( Log.w(
TAG, TAG,
"Package $packageName does not have a main launcher activity. " + "Package $packageName does not have a main launcher activity. " +
"Fallback to full app name") "Fallback to full app name"
)
return try { return try {
val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0) val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0)
packageManager.getApplicationLabel(applicationInfo) packageManager.getApplicationLabel(applicationInfo)

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 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.
*/
package com.android.systemui.media.controls.models.recommendation package com.android.systemui.media.controls.models.recommendation
import android.app.smartspace.SmartspaceTarget import android.app.smartspace.SmartspaceTarget
@@ -23,7 +39,7 @@ class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin
smartspaceMediaTargetListeners.remove(smartspaceTargetListener) smartspaceMediaTargetListeners.remove(smartspaceTargetListener)
} }
/** Updates Smartspace data and propagates it to any listeners. */ /** Updates Smartspace data and propagates it to any listeners. */
override fun onTargetsAvailable(targets: List<SmartspaceTarget>) { override fun onTargetsAvailable(targets: List<SmartspaceTarget>) {
// Filter out non-media targets. // Filter out non-media targets.
val mediaTargets = mutableListOf<SmartspaceTarget>() val mediaTargets = mutableListOf<SmartspaceTarget>()

View File

@@ -17,17 +17,15 @@
package com.android.systemui.media.controls.pipeline package com.android.systemui.media.controls.pipeline
import android.content.Context import android.content.Context
import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.InfoMediaManager import com.android.settingslib.media.InfoMediaManager
import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.LocalMediaManager
import javax.inject.Inject import javax.inject.Inject
/** /** Factory to create [LocalMediaManager] objects. */
* Factory to create [LocalMediaManager] objects. class LocalMediaManagerFactory
*/ @Inject
class LocalMediaManagerFactory @Inject constructor( constructor(
private val context: Context, private val context: Context,
private val localBluetoothManager: LocalBluetoothManager? private val localBluetoothManager: LocalBluetoothManager?
) { ) {

View File

@@ -21,11 +21,9 @@ import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import javax.inject.Inject import javax.inject.Inject
/** /** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */
* Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. class MediaDataCombineLatest @Inject constructor() :
*/ MediaDataManager.Listener, MediaDeviceManager.Listener {
class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
MediaDeviceManager.Listener {
private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf() private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
@@ -63,11 +61,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
} }
override fun onMediaDeviceChanged( override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) {
key: String,
oldKey: String?,
data: MediaDeviceData?
) {
if (oldKey != null && oldKey != key && entries.contains(oldKey)) { if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
entries[key] = entries.remove(oldKey)?.first to data entries[key] = entries.remove(oldKey)?.first to data
update(key, oldKey) update(key, oldKey)
@@ -86,9 +80,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
*/ */
fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
/** /** Remove a listener registered with addListener. */
* Remove a listener registered with addListener.
*/
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
private fun update(key: String, oldKey: String?) { private fun update(key: String, oldKey: String?) {
@@ -96,18 +88,14 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
if (entry != null && device != null) { if (entry != null && device != null) {
val data = entry.copy(device = device) val data = entry.copy(device = device)
val listenersCopy = listeners.toSet() val listenersCopy = listeners.toSet()
listenersCopy.forEach { listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) }
it.onMediaDataLoaded(key, oldKey, data)
}
} }
} }
private fun remove(key: String) { private fun remove(key: String) {
entries.remove(key)?.let { entries.remove(key)?.let {
val listenersCopy = listeners.toSet() val listenersCopy = listeners.toSet()
listenersCopy.forEach { listenersCopy.forEach { it.onMediaDataRemoved(key) }
it.onMediaDataRemoved(key)
}
} }
} }
} }

View File

@@ -23,9 +23,9 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.settings.CurrentUserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.SystemClock
@@ -37,7 +37,8 @@ import kotlin.collections.LinkedHashMap
private const val TAG = "MediaDataFilter" private const val TAG = "MediaDataFilter"
private const val DEBUG = true private const val DEBUG = true
private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = ("com.google" + private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
("com.google" +
".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity") ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
@@ -46,8 +47,8 @@ private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age
* available within this time window, smartspace recommendations will be shown instead. * available within this time window, smartspace recommendations will be shown instead.
*/ */
@VisibleForTesting @VisibleForTesting
internal val SMARTSPACE_MAX_AGE = SystemProperties internal val SMARTSPACE_MAX_AGE =
.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30)) SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
/** /**
* Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
@@ -57,7 +58,9 @@ internal val SMARTSPACE_MAX_AGE = SystemProperties
* This is added at the end of the pipeline since we may still need to handle callbacks from * This is added at the end of the pipeline since we may still need to handle callbacks from
* background users (e.g. timeouts). * background users (e.g. timeouts).
*/ */
class MediaDataFilter @Inject constructor( class MediaDataFilter
@Inject
constructor(
private val context: Context, private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
private val broadcastSender: BroadcastSender, private val broadcastSender: BroadcastSender,
@@ -79,12 +82,13 @@ class MediaDataFilter @Inject constructor(
private var reactivatedKey: String? = null private var reactivatedKey: String? = null
init { init {
userTracker = object : CurrentUserTracker(broadcastDispatcher) { userTracker =
override fun onUserSwitched(newUserId: Int) { object : CurrentUserTracker(broadcastDispatcher) {
// Post this so we can be sure lockscreenUserManager already got the broadcast override fun onUserSwitched(newUserId: Int) {
executor.execute { handleUserSwitched(newUserId) } // Post this so we can be sure lockscreenUserManager already got the broadcast
executor.execute { handleUserSwitched(newUserId) }
}
} }
}
userTracker.startTracking() userTracker.startTracking()
} }
@@ -111,9 +115,7 @@ class MediaDataFilter @Inject constructor(
userEntries.put(key, data) userEntries.put(key, data)
// Notify listeners // Notify listeners
listeners.forEach { listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
it.onMediaDataLoaded(key, oldKey, data)
}
} }
override fun onSmartspaceMediaDataLoaded( override fun onSmartspaceMediaDataLoaded(
@@ -131,14 +133,11 @@ class MediaDataFilter @Inject constructor(
smartspaceMediaData = data smartspaceMediaData = data
// Before forwarding the smartspace target, first check if we have recently inactive media // Before forwarding the smartspace target, first check if we have recently inactive media
val sorted = userEntries.toSortedMap(compareBy { val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
userEntries.get(it)?.lastActive ?: -1
})
val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
data.cardAction?.let { data.cardAction?.let {
val smartspaceMaxAgeSeconds = val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
if (smartspaceMaxAgeSeconds > 0) { if (smartspaceMaxAgeSeconds > 0) {
smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
} }
@@ -155,13 +154,21 @@ class MediaDataFilter @Inject constructor(
Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
reactivatedKey = lastActiveKey reactivatedKey = lastActiveKey
val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName, logger.logRecommendationActivated(
mediaData.instanceId) mediaData.appUid,
mediaData.packageName,
mediaData.instanceId
)
listeners.forEach { listeners.forEach {
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, it.onMediaDataLoaded(
receivedSmartspaceCardLatency = lastActiveKey,
lastActiveKey,
mediaData,
receivedSmartspaceCardLatency =
(systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
.toInt(), isSsReactivated = true) .toInt(),
isSsReactivated = true
)
} }
} }
} else { } else {
@@ -173,8 +180,10 @@ class MediaDataFilter @Inject constructor(
Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
return return
} }
logger.logRecommendationAdded(smartspaceMediaData.packageName, logger.logRecommendationAdded(
smartspaceMediaData.instanceId) smartspaceMediaData.packageName,
smartspaceMediaData.instanceId
)
listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
} }
@@ -182,9 +191,7 @@ class MediaDataFilter @Inject constructor(
allEntries.remove(key) allEntries.remove(key)
userEntries.remove(key)?.let { userEntries.remove(key)?.let {
// Only notify listeners if something actually changed // Only notify listeners if something actually changed
listeners.forEach { listeners.forEach { it.onMediaDataRemoved(key) }
it.onMediaDataRemoved(key)
}
} }
} }
@@ -197,16 +204,17 @@ class MediaDataFilter @Inject constructor(
// Notify listeners to update with actual active value // Notify listeners to update with actual active value
userEntries.get(lastActiveKey)?.let { mediaData -> userEntries.get(lastActiveKey)?.let { mediaData ->
listeners.forEach { listeners.forEach {
it.onMediaDataLoaded( it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
lastActiveKey, lastActiveKey, mediaData, immediately)
} }
} }
} }
if (smartspaceMediaData.isActive) { if (smartspaceMediaData.isActive) {
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceMediaData =
targetId = smartspaceMediaData.targetId, EMPTY_SMARTSPACE_MEDIA_DATA.copy(
instanceId = smartspaceMediaData.instanceId) targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId
)
} }
listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
} }
@@ -221,25 +229,19 @@ class MediaDataFilter @Inject constructor(
userEntries.clear() userEntries.clear()
keyCopy.forEach { keyCopy.forEach {
if (DEBUG) Log.d(TAG, "Removing $it after user change") if (DEBUG) Log.d(TAG, "Removing $it after user change")
listenersCopy.forEach { listener -> listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
listener.onMediaDataRemoved(it)
}
} }
allEntries.forEach { (key, data) -> allEntries.forEach { (key, data) ->
if (lockscreenUserManager.isCurrentProfile(data.userId)) { if (lockscreenUserManager.isCurrentProfile(data.userId)) {
if (DEBUG) Log.d(TAG, "Re-adding $key after user change") if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
userEntries.put(key, data) userEntries.put(key, data)
listenersCopy.forEach { listener -> listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
listener.onMediaDataLoaded(key, null, data)
}
} }
} }
} }
/** /** Invoked when the user has dismissed the media carousel */
* Invoked when the user has dismissed the media carousel
*/
fun onSwipeToDismiss() { fun onSwipeToDismiss() {
if (DEBUG) Log.d(TAG, "Media carousel swiped away") if (DEBUG) Log.d(TAG, "Media carousel swiped away")
val mediaKeys = userEntries.keys.toSet() val mediaKeys = userEntries.keys.toSet()
@@ -250,55 +252,52 @@ class MediaDataFilter @Inject constructor(
if (smartspaceMediaData.isActive) { if (smartspaceMediaData.isActive) {
val dismissIntent = smartspaceMediaData.dismissIntent val dismissIntent = smartspaceMediaData.dismissIntent
if (dismissIntent == null) { if (dismissIntent == null) {
Log.w(TAG, "Cannot create dismiss action click action: " + Log.w(
"extras missing dismiss_intent.") TAG,
} else if (dismissIntent.getComponent() != null && "Cannot create dismiss action click action: " + "extras missing dismiss_intent."
dismissIntent.getComponent().getClassName() )
== EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) { } else if (
dismissIntent.getComponent() != null &&
dismissIntent.getComponent().getClassName() ==
EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
) {
// Dismiss the card Smartspace data through Smartspace trampoline activity. // Dismiss the card Smartspace data through Smartspace trampoline activity.
context.startActivity(dismissIntent) context.startActivity(dismissIntent)
} else { } else {
broadcastSender.sendBroadcast(dismissIntent) broadcastSender.sendBroadcast(dismissIntent)
} }
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceMediaData =
targetId = smartspaceMediaData.targetId, EMPTY_SMARTSPACE_MEDIA_DATA.copy(
instanceId = smartspaceMediaData.instanceId) targetId = smartspaceMediaData.targetId,
mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, instanceId = smartspaceMediaData.instanceId
delay = 0L) )
mediaDataManager.dismissSmartspaceRecommendation(
smartspaceMediaData.targetId,
delay = 0L
)
} }
} }
/** /** Are there any active media entries, including the recommendation? */
* Are there any active media entries, including the recommendation? fun hasActiveMediaOrRecommendation() =
*/ userEntries.any { it.value.active } ||
fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } ||
(smartspaceMediaData.isActive && (smartspaceMediaData.isActive &&
(smartspaceMediaData.isValid() || reactivatedKey != null)) (smartspaceMediaData.isValid() || reactivatedKey != null))
/** /** Are there any media entries we should display? */
* Are there any media entries we should display? fun hasAnyMediaOrRecommendation() =
*/ userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() ||
(smartspaceMediaData.isActive && smartspaceMediaData.isValid())
/** /** Are there any media notifications active (excluding the recommendation)? */
* Are there any media notifications active (excluding the recommendation)?
*/
fun hasActiveMedia() = userEntries.any { it.value.active } fun hasActiveMedia() = userEntries.any { it.value.active }
/** /** Are there any media entries we should display (excluding the recommendation)? */
* Are there any media entries we should display (excluding the recommendation)?
*/
fun hasAnyMedia() = userEntries.isNotEmpty() fun hasAnyMedia() = userEntries.isNotEmpty()
/** /** Add a listener for filtered [MediaData] changes */
* Add a listener for filtered [MediaData] changes
*/
fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
/** /** Remove a listener that was registered with addListener */
* Remove a listener that was registered with addListener
*/
fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
/** /**
@@ -318,8 +317,6 @@ class MediaDataFilter @Inject constructor(
val now = systemClock.elapsedRealtime() val now = systemClock.elapsedRealtime()
val lastActiveKey = sortedEntries.lastKey() // most recently active val lastActiveKey = sortedEntries.lastKey() // most recently active
return sortedEntries.get(lastActiveKey)?.let { return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE
now - it.lastActive
} ?: Long.MAX_VALUE
} }
} }

View File

@@ -36,10 +36,10 @@ import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
@@ -51,10 +51,10 @@ private const val PLAYBACK_TYPE_UNKNOWN = 0
private const val TAG = "MediaDeviceManager" private const val TAG = "MediaDeviceManager"
private const val DEBUG = true private const val DEBUG = true
/** /** Provides information about the route (ie. device) where playback is occurring. */
* Provides information about the route (ie. device) where playback is occurring. class MediaDeviceManager
*/ @Inject
class MediaDeviceManager @Inject constructor( constructor(
private val context: Context, private val context: Context,
private val controllerFactory: MediaControllerFactory, private val controllerFactory: MediaControllerFactory,
private val localMediaManagerFactory: LocalMediaManagerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory,
@@ -74,14 +74,10 @@ class MediaDeviceManager @Inject constructor(
dumpManager.registerDumpable(javaClass.name, this) dumpManager.registerDumpable(javaClass.name, this)
} }
/** /** Add a listener for changes to the media route (ie. device). */
* Add a listener for changes to the media route (ie. device).
*/
fun addListener(listener: Listener) = listeners.add(listener) fun addListener(listener: Listener) = listeners.add(listener)
/** /** Remove a listener that has been registered with addListener. */
* Remove a listener that has been registered with addListener.
*/
fun removeListener(listener: Listener) = listeners.remove(listener) fun removeListener(listener: Listener) = listeners.remove(listener)
override fun onMediaDataLoaded( override fun onMediaDataLoaded(
@@ -105,19 +101,11 @@ class MediaDeviceManager @Inject constructor(
processDevice(key, oldKey, data.device) processDevice(key, oldKey, data.device)
return return
} }
val controller = data.token?.let { val controller = data.token?.let { controllerFactory.create(it) }
controllerFactory.create(it)
}
val localMediaManager = localMediaManagerFactory.create(data.packageName) val localMediaManager = localMediaManagerFactory.create(data.packageName)
val muteAwaitConnectionManager = val muteAwaitConnectionManager =
muteAwaitConnectionManagerFactory.create(localMediaManager) muteAwaitConnectionManagerFactory.create(localMediaManager)
entry = Entry( entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
key,
oldKey,
controller,
localMediaManager,
muteAwaitConnectionManager
)
entries[key] = entry entries[key] = entry
entry.start() entry.start()
} }
@@ -126,11 +114,7 @@ class MediaDeviceManager @Inject constructor(
override fun onMediaDataRemoved(key: String) { override fun onMediaDataRemoved(key: String) {
val token = entries.remove(key) val token = entries.remove(key)
token?.stop() token?.stop()
token?.let { token?.let { listeners.forEach { it.onKeyRemoved(key) } }
listeners.forEach {
it.onKeyRemoved(key)
}
}
} }
override fun dump(pw: PrintWriter, args: Array<String>) { override fun dump(pw: PrintWriter, args: Array<String>) {
@@ -145,9 +129,7 @@ class MediaDeviceManager @Inject constructor(
@MainThread @MainThread
private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
listeners.forEach { listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
it.onMediaDeviceChanged(key, oldKey, device)
}
} }
interface Listener { interface Listener {
@@ -163,8 +145,10 @@ class MediaDeviceManager @Inject constructor(
val controller: MediaController?, val controller: MediaController?,
val localMediaManager: LocalMediaManager, val localMediaManager: LocalMediaManager,
val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager? val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
) : LocalMediaManager.DeviceCallback, MediaController.Callback(), ) :
BluetoothLeBroadcast.Callback { LocalMediaManager.DeviceCallback,
MediaController.Callback(),
BluetoothLeBroadcast.Callback {
val token val token
get() = controller?.sessionToken get() = controller?.sessionToken
@@ -175,54 +159,52 @@ class MediaDeviceManager @Inject constructor(
val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
if (!started || !sameWithoutIcon) { if (!started || !sameWithoutIcon) {
field = value field = value
fgExecutor.execute { fgExecutor.execute { processDevice(key, oldKey, value) }
processDevice(key, oldKey, value)
}
} }
} }
// A device that is not yet connected but is expected to connect imminently. Because it's // A device that is not yet connected but is expected to connect imminently. Because it's
// expected to connect imminently, it should be displayed as the current device. // expected to connect imminently, it should be displayed as the current device.
private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
private var broadcastDescription: String? = null private var broadcastDescription: String? = null
private val configListener = object : ConfigurationController.ConfigurationListener { private val configListener =
override fun onLocaleListChanged() { object : ConfigurationController.ConfigurationListener {
updateCurrent() override fun onLocaleListChanged() {
updateCurrent()
}
} }
}
@AnyThread @AnyThread
fun start() = bgExecutor.execute { fun start() =
if (!started) { bgExecutor.execute {
localMediaManager.registerCallback(this) if (!started) {
localMediaManager.startScan() localMediaManager.registerCallback(this)
muteAwaitConnectionManager?.startListening() localMediaManager.startScan()
playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN muteAwaitConnectionManager?.startListening()
controller?.registerCallback(this) playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
updateCurrent() controller?.registerCallback(this)
started = true updateCurrent()
configurationController.addCallback(configListener) started = true
configurationController.addCallback(configListener)
}
} }
}
@AnyThread @AnyThread
fun stop() = bgExecutor.execute { fun stop() =
if (started) { bgExecutor.execute {
started = false if (started) {
controller?.unregisterCallback(this) started = false
localMediaManager.stopScan() controller?.unregisterCallback(this)
localMediaManager.unregisterCallback(this) localMediaManager.stopScan()
muteAwaitConnectionManager?.stopListening() localMediaManager.unregisterCallback(this)
configurationController.removeCallback(configListener) muteAwaitConnectionManager?.stopListening()
configurationController.removeCallback(configListener)
}
} }
}
fun dump(pw: PrintWriter) { fun dump(pw: PrintWriter) {
val routingSession = controller?.let { val routingSession =
mr2manager.getRoutingSessionForMediaController(it) controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
} val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
val selectedRoutes = routingSession?.let {
mr2manager.getSelectedRoutes(it)
}
with(pw) { with(pw) {
println(" current device is ${current?.name}") println(" current device is ${current?.name}")
val type = controller?.playbackInfo?.playbackType val type = controller?.playbackInfo?.playbackType
@@ -242,14 +224,11 @@ class MediaDeviceManager @Inject constructor(
updateCurrent() updateCurrent()
} }
override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { override fun onDeviceListUpdate(devices: List<MediaDevice>?) =
updateCurrent() bgExecutor.execute { updateCurrent() }
}
override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
bgExecutor.execute { bgExecutor.execute { updateCurrent() }
updateCurrent()
}
} }
override fun onAboutToConnectDeviceAdded( override fun onAboutToConnectDeviceAdded(
@@ -257,14 +236,17 @@ class MediaDeviceManager @Inject constructor(
deviceName: String, deviceName: String,
deviceIcon: Drawable? deviceIcon: Drawable?
) { ) {
aboutToConnectDeviceOverride = AboutToConnectDevice( aboutToConnectDeviceOverride =
fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), AboutToConnectDevice(
backupMediaDeviceData = MediaDeviceData( fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
/* enabled */ enabled = true, backupMediaDeviceData =
/* icon */ deviceIcon, MediaDeviceData(
/* name */ deviceName, /* enabled */ enabled = true,
/* showBroadcastButton */ showBroadcastButton = false) /* icon */ deviceIcon,
) /* name */ deviceName,
/* showBroadcastButton */ showBroadcastButton = false
)
)
updateCurrent() updateCurrent()
} }
@@ -291,8 +273,11 @@ class MediaDeviceManager @Inject constructor(
metadata: BluetoothLeBroadcastMetadata metadata: BluetoothLeBroadcastMetadata
) { ) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + Log.d(
"metadata = $metadata") TAG,
"onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
"metadata = $metadata"
)
} }
updateCurrent() updateCurrent()
} }
@@ -319,8 +304,10 @@ class MediaDeviceManager @Inject constructor(
override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " + Log.d(
"broadcastId = $broadcastId") TAG,
"onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId"
)
} }
} }
@@ -331,34 +318,45 @@ class MediaDeviceManager @Inject constructor(
@WorkerThread @WorkerThread
private fun updateCurrent() { private fun updateCurrent() {
if (isLeAudioBroadcastEnabled()) { if (isLeAudioBroadcastEnabled()) {
current = MediaDeviceData( current =
MediaDeviceData(
/* enabled */ true, /* enabled */ true,
/* icon */ context.getDrawable(R.drawable.settings_input_antenna), /* icon */ context.getDrawable(R.drawable.settings_input_antenna),
/* name */ broadcastDescription, /* name */ broadcastDescription,
/* intent */ null, /* intent */ null,
/* showBroadcastButton */ showBroadcastButton = true) /* showBroadcastButton */ showBroadcastButton = true
)
} else { } else {
val aboutToConnect = aboutToConnectDeviceOverride val aboutToConnect = aboutToConnectDeviceOverride
if (aboutToConnect != null && if (
aboutToConnect != null &&
aboutToConnect.fullMediaDevice == null && aboutToConnect.fullMediaDevice == null &&
aboutToConnect.backupMediaDeviceData != null) { aboutToConnect.backupMediaDeviceData != null
) {
// Only use [backupMediaDeviceData] when we don't have [fullMediaDevice]. // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
current = aboutToConnect.backupMediaDeviceData current = aboutToConnect.backupMediaDeviceData
return return
} }
val device = aboutToConnect?.fullMediaDevice val device =
?: localMediaManager.currentConnectedDevice aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
// If we have a controller but get a null route, then don't trust the device // If we have a controller but get a null route, then don't trust the device
val enabled = device != null && (controller == null || route != null) val enabled = device != null && (controller == null || route != null)
val name = if (controller == null || route != null) { val name =
route?.name?.toString() ?: device?.name if (controller == null || route != null) {
} else { route?.name?.toString() ?: device?.name
null } else {
} null
current = MediaDeviceData(enabled, device?.iconWithoutBackground, name, }
id = device?.id, showBroadcastButton = false) current =
MediaDeviceData(
enabled,
device?.iconWithoutBackground,
name,
id = device?.id,
showBroadcastButton = false
)
} }
} }
@@ -388,13 +386,16 @@ class MediaDeviceManager @Inject constructor(
// unexpected result. // unexpected result.
// Check the current media app's name is the same with current broadcast app's name // Check the current media app's name is the same with current broadcast app's name
// or not. // or not.
var mediaApp = MediaDataUtils.getAppLabel( var mediaApp =
context, localMediaManager.packageName, MediaDataUtils.getAppLabel(
context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)) context,
localMediaManager.packageName,
context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
)
var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
if (isCurrentBroadcastedApp) { if (isCurrentBroadcastedApp) {
broadcastDescription = context.getString( broadcastDescription =
R.string.broadcasting_description_is_broadcasting) context.getString(R.string.broadcasting_description_is_broadcasting)
} else { } else {
broadcastDescription = currentBroadcastedApp broadcastDescription = currentBroadcastedApp
} }
@@ -407,9 +408,9 @@ class MediaDeviceManager @Inject constructor(
* [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information. * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
* *
* @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
* non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
* @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
* information required to display the device. Only use if [fullMediaDevice] is null. * information required to display the device. Only use if [fullMediaDevice] is null.
*/ */
private data class AboutToConnectDevice( private data class AboutToConnectDevice(
val fullMediaDevice: MediaDevice? = null, val fullMediaDevice: MediaDevice? = null,

View File

@@ -40,7 +40,9 @@ private const val TAG = "MediaSessionBasedFilter"
* sessions. In this situation, there should only be a media object for the remote session. To * sessions. In this situation, there should only be a media object for the remote session. To
* achieve this, update events for the local session need to be filtered. * achieve this, update events for the local session need to be filtered.
*/ */
class MediaSessionBasedFilter @Inject constructor( class MediaSessionBasedFilter
@Inject
constructor(
context: Context, context: Context,
private val sessionManager: MediaSessionManager, private val sessionManager: MediaSessionManager,
@Main private val foregroundExecutor: Executor, @Main private val foregroundExecutor: Executor,
@@ -52,7 +54,7 @@ class MediaSessionBasedFilter @Inject constructor(
// Keep track of MediaControllers for a given package to check if an app is casting and it // Keep track of MediaControllers for a given package to check if an app is casting and it
// filter loaded events for local sessions. // filter loaded events for local sessions.
private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> = private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> =
LinkedHashMap() LinkedHashMap()
// Keep track of the key used for the session tokens. This information is used to know when to // Keep track of the key used for the session tokens. This information is used to know when to
// dispatch a removed event so that a media object for a local session will be removed. // dispatch a removed event so that a media object for a local session will be removed.
@@ -61,11 +63,12 @@ class MediaSessionBasedFilter @Inject constructor(
// Keep track of which media session tokens have associated notifications. // Keep track of which media session tokens have associated notifications.
private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf() private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { private val sessionListener =
override fun onActiveSessionsChanged(controllers: List<MediaController>) { object : MediaSessionManager.OnActiveSessionsChangedListener {
handleControllersChanged(controllers) override fun onActiveSessionsChanged(controllers: List<MediaController>) {
handleControllersChanged(controllers)
}
} }
}
init { init {
backgroundExecutor.execute { backgroundExecutor.execute {
@@ -75,14 +78,10 @@ class MediaSessionBasedFilter @Inject constructor(
} }
} }
/** /** Add a listener for filtered [MediaData] changes */
* Add a listener for filtered [MediaData] changes
*/
fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
/** /** Remove a listener that was registered with addListener */
* Remove a listener that was registered with addListener
*/
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
/** /**
@@ -102,31 +101,32 @@ class MediaSessionBasedFilter @Inject constructor(
isSsReactivated: Boolean isSsReactivated: Boolean
) { ) {
backgroundExecutor.execute { backgroundExecutor.execute {
data.token?.let { data.token?.let { tokensWithNotifications.add(it) }
tokensWithNotifications.add(it)
}
val isMigration = oldKey != null && key != oldKey val isMigration = oldKey != null && key != oldKey
if (isMigration) { if (isMigration) {
keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
} }
if (data.token != null) { if (data.token != null) {
keyedTokens.get(key)?.let { keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
tokens -> ?: run {
tokens.add(data.token) val tokens = mutableSetOf(data.token)
} ?: run { keyedTokens.put(key, tokens)
val tokens = mutableSetOf(data.token) }
keyedTokens.put(key, tokens)
}
} }
// Determine if an app is casting by checking if it has a session with playback type // Determine if an app is casting by checking if it has a session with playback type
// PLAYBACK_TYPE_REMOTE. // PLAYBACK_TYPE_REMOTE.
val remoteControllers = packageControllers.get(data.packageName)?.filter { val remoteControllers =
it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE packageControllers.get(data.packageName)?.filter {
} it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
}
// Limiting search to only apps with a single remote session. // Limiting search to only apps with a single remote session.
val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
if (isMigration || remote == null || remote.sessionToken == data.token || if (
!tokensWithNotifications.contains(remote.sessionToken)) { isMigration ||
remote == null ||
remote.sessionToken == data.token ||
!tokensWithNotifications.contains(remote.sessionToken)
) {
// Not filtering in this case. Passing the event along to listeners. // Not filtering in this case. Passing the event along to listeners.
dispatchMediaDataLoaded(key, oldKey, data, immediately) dispatchMediaDataLoaded(key, oldKey, data, immediately)
} else { } else {
@@ -148,9 +148,7 @@ class MediaSessionBasedFilter @Inject constructor(
data: SmartspaceMediaData, data: SmartspaceMediaData,
shouldPrioritize: Boolean shouldPrioritize: Boolean
) { ) {
backgroundExecutor.execute { backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
dispatchSmartspaceMediaDataLoaded(key, data)
}
} }
override fun onMediaDataRemoved(key: String) { override fun onMediaDataRemoved(key: String) {
@@ -162,9 +160,7 @@ class MediaSessionBasedFilter @Inject constructor(
} }
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
backgroundExecutor.execute { backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) }
dispatchSmartspaceMediaDataRemoved(key, immediately)
}
} }
private fun dispatchMediaDataLoaded( private fun dispatchMediaDataLoaded(
@@ -179,9 +175,7 @@ class MediaSessionBasedFilter @Inject constructor(
} }
private fun dispatchMediaDataRemoved(key: String) { private fun dispatchMediaDataRemoved(key: String) {
foregroundExecutor.execute { foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } }
listeners.toSet().forEach { it.onMediaDataRemoved(key) }
}
} }
private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
@@ -198,15 +192,12 @@ class MediaSessionBasedFilter @Inject constructor(
private fun handleControllersChanged(controllers: List<MediaController>) { private fun handleControllersChanged(controllers: List<MediaController>) {
packageControllers.clear() packageControllers.clear()
controllers.forEach { controllers.forEach { controller ->
controller -> packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) }
packageControllers.get(controller.packageName)?.let { ?: run {
tokens -> val tokens = mutableListOf(controller)
tokens.add(controller) packageControllers.put(controller.packageName, tokens)
} ?: run { }
val tokens = mutableListOf(controller)
packageControllers.put(controller.packageName, tokens)
}
} }
tokensWithNotifications.retainAll(controllers.map { it.sessionToken }) tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
} }

View File

@@ -22,8 +22,8 @@ import android.os.SystemProperties
import com.android.internal.annotations.VisibleForTesting import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -33,18 +33,18 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@VisibleForTesting @VisibleForTesting
val PAUSED_MEDIA_TIMEOUT = SystemProperties val PAUSED_MEDIA_TIMEOUT =
.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10)) SystemProperties.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
@VisibleForTesting @VisibleForTesting
val RESUME_MEDIA_TIMEOUT = SystemProperties val RESUME_MEDIA_TIMEOUT =
.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3)) SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
/** /** Controller responsible for keeping track of playback states and expiring inactive streams. */
* Controller responsible for keeping track of playback states and expiring inactive streams.
*/
@SysUISingleton @SysUISingleton
class MediaTimeoutListener @Inject constructor( class MediaTimeoutListener
@Inject
constructor(
private val mediaControllerFactory: MediaControllerFactory, private val mediaControllerFactory: MediaControllerFactory,
@Main private val mainExecutor: DelayableExecutor, @Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger, private val logger: MediaTimeoutLogger,
@@ -58,7 +58,9 @@ class MediaTimeoutListener @Inject constructor(
* Callback representing that a media object is now expired: * Callback representing that a media object is now expired:
* @param key Media control unique identifier * @param key Media control unique identifier
* @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media, * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media,
* ```
* or {@code RESUME_MEDIA_TIMEOUT} for resume media * or {@code RESUME_MEDIA_TIMEOUT} for resume media
* ```
*/ */
lateinit var timeoutCallback: (String, Boolean) -> Unit lateinit var timeoutCallback: (String, Boolean) -> Unit
@@ -70,21 +72,25 @@ class MediaTimeoutListener @Inject constructor(
lateinit var stateCallback: (String, PlaybackState) -> Unit lateinit var stateCallback: (String, PlaybackState) -> Unit
init { init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener { statusBarStateController.addCallback(
override fun onDozingChanged(isDozing: Boolean) { object : StatusBarStateController.StateListener {
if (!isDozing) { override fun onDozingChanged(isDozing: Boolean) {
// Check whether any timeouts should have expired if (!isDozing) {
mediaListeners.forEach { (key, listener) -> // Check whether any timeouts should have expired
if (listener.cancellation != null && mediaListeners.forEach { (key, listener) ->
listener.expiration <= systemClock.elapsedRealtime()) { if (
// We dozed too long - timeout now, and cancel the pending one listener.cancellation != null &&
listener.expireMediaTimeout(key, "timeout happened while dozing") listener.expiration <= systemClock.elapsedRealtime()
listener.doTimeout() ) {
// We dozed too long - timeout now, and cancel the pending one
listener.expireMediaTimeout(key, "timeout happened while dozing")
listener.doTimeout()
}
} }
} }
} }
} }
}) )
} }
override fun onMediaDataLoaded( override fun onMediaDataLoaded(
@@ -147,10 +153,8 @@ class MediaTimeoutListener @Inject constructor(
return mediaListeners[key]?.timedOut ?: false return mediaListeners[key]?.timedOut ?: false
} }
private inner class PlaybackStateListener( private inner class PlaybackStateListener(var key: String, data: MediaData) :
var key: String, MediaController.Callback() {
data: MediaData
) : MediaController.Callback() {
var timedOut = false var timedOut = false
var lastState: PlaybackState? = null var lastState: PlaybackState? = null
@@ -164,11 +168,12 @@ class MediaTimeoutListener @Inject constructor(
mediaController?.unregisterCallback(this) mediaController?.unregisterCallback(this)
field = value field = value
val token = field.token val token = field.token
mediaController = if (token != null) { mediaController =
mediaControllerFactory.create(token) if (token != null) {
} else { mediaControllerFactory.create(token)
null } else {
} null
}
mediaController?.registerCallback(this) mediaController?.registerCallback(this)
// Let's register the cancellations, but not dispatch events now. // Let's register the cancellations, but not dispatch events now.
// Timeouts didn't happen yet and reentrant events are troublesome. // Timeouts didn't happen yet and reentrant events are troublesome.
@@ -214,7 +219,8 @@ class MediaTimeoutListener @Inject constructor(
logger.logPlaybackState(key, state) logger.logPlaybackState(key, state)
val playingStateSame = (state?.state?.isPlaying() == isPlaying()) val playingStateSame = (state?.state?.isPlaying() == isPlaying())
val actionsSame = (lastState?.actions == state?.actions) && val actionsSame =
(lastState?.actions == state?.actions) &&
areCustomActionListsEqual(lastState?.customActions, state?.customActions) areCustomActionListsEqual(lastState?.customActions, state?.customActions)
val resumptionChanged = resumption != mediaData.resumption val resumptionChanged = resumption != mediaData.resumption
@@ -239,15 +245,14 @@ class MediaTimeoutListener @Inject constructor(
return return
} }
expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption") expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption")
val timeout = if (mediaData.resumption) { val timeout =
RESUME_MEDIA_TIMEOUT if (mediaData.resumption) {
} else { RESUME_MEDIA_TIMEOUT
PAUSED_MEDIA_TIMEOUT } else {
} PAUSED_MEDIA_TIMEOUT
}
expiration = systemClock.elapsedRealtime() + timeout expiration = systemClock.elapsedRealtime() + timeout
cancellation = mainExecutor.executeDelayed({ cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
doTimeout()
}, timeout)
} else { } else {
expireMediaTimeout(key, "playback started - $state, $key") expireMediaTimeout(key, "playback started - $state, $key")
timedOut = false timedOut = false
@@ -303,9 +308,11 @@ class MediaTimeoutListener @Inject constructor(
firstAction: PlaybackState.CustomAction, firstAction: PlaybackState.CustomAction,
secondAction: PlaybackState.CustomAction secondAction: PlaybackState.CustomAction
): Boolean { ): Boolean {
if (firstAction.action != secondAction.action || if (
firstAction.action != secondAction.action ||
firstAction.name != secondAction.name || firstAction.name != secondAction.name ||
firstAction.icon != secondAction.icon) { firstAction.icon != secondAction.icon
) {
return false return false
} }

View File

@@ -22,140 +22,91 @@ import com.android.systemui.log.dagger.MediaTimeoutListenerLog
import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel import com.android.systemui.plugins.log.LogLevel
import javax.inject.Inject import javax.inject.Inject
private const val TAG = "MediaTimeout" private const val TAG = "MediaTimeout"
/** /** A buffered log for [MediaTimeoutListener] events */
* A buffered log for [MediaTimeoutListener] events
*/
@SysUISingleton @SysUISingleton
class MediaTimeoutLogger @Inject constructor( class MediaTimeoutLogger
@MediaTimeoutListenerLog private val buffer: LogBuffer @Inject
) { constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) {
fun logReuseListener(key: String) = buffer.log( fun logReuseListener(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "reuse listener: $str1" })
LogLevel.DEBUG,
{
str1 = key
},
{
"reuse listener: $str1"
}
)
fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log( fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.DEBUG,
str1 = oldKey {
str2 = newKey str1 = oldKey
bool1 = hadListener str2 = newKey
}, bool1 = hadListener
{ },
"migrate from $str1 to $str2, had listener? $bool1" { "migrate from $str1 to $str2, had listener? $bool1" }
} )
)
fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log( fun logUpdateListener(key: String, wasPlaying: Boolean) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.DEBUG,
str1 = key {
bool1 = wasPlaying str1 = key
}, bool1 = wasPlaying
{ },
"updating $str1, was playing? $bool1" { "updating $str1, was playing? $bool1" }
} )
)
fun logDelayedUpdate(key: String) = buffer.log( fun logDelayedUpdate(key: String) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.DEBUG,
str1 = key { str1 = key },
}, { "deliver delayed playback state for $str1" }
{ )
"deliver delayed playback state for $str1"
}
)
fun logSessionDestroyed(key: String) = buffer.log( fun logSessionDestroyed(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "session destroyed $str1" })
LogLevel.DEBUG,
{
str1 = key
},
{
"session destroyed $str1"
}
)
fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log( fun logPlaybackState(key: String, state: PlaybackState?) =
TAG, buffer.log(
LogLevel.VERBOSE,
{
str1 = key
str2 = state?.toString()
},
{
"state update: key=$str1 state=$str2"
}
)
fun logStateCallback(key: String) = buffer.log(
TAG, TAG,
LogLevel.VERBOSE, LogLevel.VERBOSE,
{ {
str1 = key str1 = key
str2 = state?.toString()
}, },
{ "state update: key=$str1 state=$str2" }
)
fun logStateCallback(key: String) =
buffer.log(TAG, LogLevel.VERBOSE, { str1 = key }, { "dispatching state update for $key" })
fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) =
buffer.log(
TAG,
LogLevel.DEBUG,
{ {
"dispatching state update for $key" str1 = key
} bool1 = playing
) bool2 = resumption
},
{ "schedule timeout $str1, playing=$bool1 resumption=$bool2" }
)
fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log( fun logCancelIgnored(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "cancellation already exists for $str1" })
LogLevel.DEBUG,
{
str1 = key
bool1 = playing
bool2 = resumption
},
{
"schedule timeout $str1, playing=$bool1 resumption=$bool2"
}
)
fun logCancelIgnored(key: String) = buffer.log( fun logTimeout(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "execute timeout for $str1" })
LogLevel.DEBUG,
{
str1 = key
},
{
"cancellation already exists for $str1"
}
)
fun logTimeout(key: String) = buffer.log( fun logTimeoutCancelled(key: String, reason: String) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.VERBOSE,
str1 = key {
}, str1 = key
{ str2 = reason
"execute timeout for $str1" },
} { "media timeout cancelled for $str1, reason: $str2" }
) )
}
fun logTimeoutCancelled(key: String, reason: String) = buffer.log(
TAG,
LogLevel.VERBOSE,
{
str1 = key
str2 = reason
},
{
"media timeout cancelled for $str1, reason: $str2"
}
)
}

View File

@@ -50,7 +50,9 @@ private const val MEDIA_PREFERENCES = "media_control_prefs"
private const val MEDIA_PREFERENCE_KEY = "browser_components_" private const val MEDIA_PREFERENCE_KEY = "browser_components_"
@SysUISingleton @SysUISingleton
class MediaResumeListener @Inject constructor( class MediaResumeListener
@Inject
constructor(
private val context: Context, private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor, @Background private val backgroundExecutor: Executor,
@@ -62,7 +64,7 @@ class MediaResumeListener @Inject constructor(
private var useMediaResumption: Boolean = Utils.useMediaResumption(context) private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> = private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
ConcurrentLinkedQueue() ConcurrentLinkedQueue()
private lateinit var mediaDataManager: MediaDataManager private lateinit var mediaDataManager: MediaDataManager
@@ -75,40 +77,49 @@ class MediaResumeListener @Inject constructor(
private var currentUserId: Int = context.userId private var currentUserId: Int = context.userId
@VisibleForTesting @VisibleForTesting
val userChangeReceiver = object : BroadcastReceiver() { val userChangeReceiver =
override fun onReceive(context: Context, intent: Intent) { object : BroadcastReceiver() {
if (Intent.ACTION_USER_UNLOCKED == intent.action) { override fun onReceive(context: Context, intent: Intent) {
loadMediaResumptionControls() if (Intent.ACTION_USER_UNLOCKED == intent.action) {
} else if (Intent.ACTION_USER_SWITCHED == intent.action) { loadMediaResumptionControls()
currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) } else if (Intent.ACTION_USER_SWITCHED == intent.action) {
loadSavedComponents() currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
loadSavedComponents()
}
} }
} }
}
private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() { private val mediaBrowserCallback =
override fun addTrack( object : ResumeMediaBrowser.Callback() {
desc: MediaDescription, override fun addTrack(
component: ComponentName, desc: MediaDescription,
browser: ResumeMediaBrowser component: ComponentName,
) { browser: ResumeMediaBrowser
val token = browser.token ) {
val appIntent = browser.appIntent val token = browser.token
val pm = context.getPackageManager() val appIntent = browser.appIntent
var appName: CharSequence = component.packageName val pm = context.getPackageManager()
val resumeAction = getResumeAction(component) var appName: CharSequence = component.packageName
try { val resumeAction = getResumeAction(component)
appName = pm.getApplicationLabel( try {
pm.getApplicationInfo(component.packageName, 0)) appName =
} catch (e: PackageManager.NameNotFoundException) { pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0))
Log.e(TAG, "Error getting package information", e) } catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Error getting package information", e)
}
Log.d(TAG, "Adding resume controls $desc")
mediaDataManager.addResumptionControls(
currentUserId,
desc,
resumeAction,
token,
appName.toString(),
appIntent,
component.packageName
)
} }
Log.d(TAG, "Adding resume controls $desc")
mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token,
appName.toString(), appIntent, component.packageName)
} }
}
init { init {
if (useMediaResumption) { if (useMediaResumption) {
@@ -116,8 +127,12 @@ class MediaResumeListener @Inject constructor(
val unlockFilter = IntentFilter() val unlockFilter = IntentFilter()
unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
unlockFilter.addAction(Intent.ACTION_USER_SWITCHED) unlockFilter.addAction(Intent.ACTION_USER_SWITCHED)
broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null, broadcastDispatcher.registerReceiver(
UserHandle.ALL) userChangeReceiver,
unlockFilter,
null,
UserHandle.ALL
)
loadSavedComponents() loadSavedComponents()
} }
} }
@@ -126,12 +141,15 @@ class MediaResumeListener @Inject constructor(
mediaDataManager = manager mediaDataManager = manager
// Add listener for resumption setting changes // Add listener for resumption setting changes
tunerService.addTunable(object : TunerService.Tunable { tunerService.addTunable(
override fun onTuningChanged(key: String?, newValue: String?) { object : TunerService.Tunable {
useMediaResumption = Utils.useMediaResumption(context) override fun onTuningChanged(key: String?, newValue: String?) {
mediaDataManager.setMediaResumptionEnabled(useMediaResumption) useMediaResumption = Utils.useMediaResumption(context)
} mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
}, Settings.Secure.MEDIA_CONTROLS_RESUME) }
},
Settings.Secure.MEDIA_CONTROLS_RESUME
)
} }
private fun loadSavedComponents() { private fun loadSavedComponents() {
@@ -139,8 +157,10 @@ class MediaResumeListener @Inject constructor(
resumeComponents.clear() resumeComponents.clear()
val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null) val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex()) val components =
?.dropLastWhile { it.isEmpty() } listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile {
it.isEmpty()
}
var needsUpdate = false var needsUpdate = false
components?.forEach { components?.forEach {
val info = it.split("/") val info = it.split("/")
@@ -148,17 +168,18 @@ class MediaResumeListener @Inject constructor(
val className = info[1] val className = info[1]
val component = ComponentName(packageName, className) val component = ComponentName(packageName, className)
val lastPlayed = if (info.size == 3) { val lastPlayed =
try { if (info.size == 3) {
info[2].toLong() try {
} catch (e: NumberFormatException) { info[2].toLong()
} catch (e: NumberFormatException) {
needsUpdate = true
systemClock.currentTimeMillis()
}
} else {
needsUpdate = true needsUpdate = true
systemClock.currentTimeMillis() systemClock.currentTimeMillis()
} }
} else {
needsUpdate = true
systemClock.currentTimeMillis()
}
resumeComponents.add(component to lastPlayed) resumeComponents.add(component to lastPlayed)
} }
Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
@@ -169,9 +190,7 @@ class MediaResumeListener @Inject constructor(
} }
} }
/** /** Load controls for resuming media, if available */
* Load controls for resuming media, if available
*/
private fun loadMediaResumptionControls() { private fun loadMediaResumptionControls() {
if (!useMediaResumption) { if (!useMediaResumption) {
return return
@@ -207,9 +226,7 @@ class MediaResumeListener @Inject constructor(
val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
val resumeInfo = pm.queryIntentServices(serviceIntent, 0) val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
val inf = resumeInfo?.filter { val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
it.serviceInfo.packageName == data.packageName
}
if (inf != null && inf.size > 0) { if (inf != null && inf.size > 0) {
backgroundExecutor.execute { backgroundExecutor.execute {
tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
@@ -230,7 +247,8 @@ class MediaResumeListener @Inject constructor(
Log.d(TAG, "Testing if we can connect to $componentName") Log.d(TAG, "Testing if we can connect to $componentName")
// Set null action to prevent additional attempts to connect // Set null action to prevent additional attempts to connect
mediaDataManager.setResumeAction(key, null) mediaDataManager.setResumeAction(key, null)
mediaBrowser = mediaBrowserFactory.create( mediaBrowser =
mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() { object : ResumeMediaBrowser.Callback() {
override fun onConnected() { override fun onConnected() {
Log.d(TAG, "Connected to $componentName") Log.d(TAG, "Connected to $componentName")
@@ -253,7 +271,8 @@ class MediaResumeListener @Inject constructor(
mediaBrowser = null mediaBrowser = null
} }
}, },
componentName) componentName
)
mediaBrowser?.testConnection() mediaBrowser?.testConnection()
} }
@@ -288,9 +307,7 @@ class MediaResumeListener @Inject constructor(
prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply() prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
} }
/** /** Get a runnable which will resume media playback */
* Get a runnable which will resume media playback
*/
private fun getResumeAction(componentName: ComponentName): Runnable { private fun getResumeAction(componentName: ComponentName): Runnable {
return Runnable { return Runnable {
mediaBrowser = mediaBrowserFactory.create(null, componentName) mediaBrowser = mediaBrowserFactory.create(null, componentName)
@@ -299,8 +316,6 @@ class MediaResumeListener @Inject constructor(
} }
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply { pw.apply { println("resumeComponents: $resumeComponents") }
println("resumeComponents: $resumeComponents")
}
} }
} }

View File

@@ -25,50 +25,49 @@ import javax.inject.Inject
/** A logger for events in [ResumeMediaBrowser]. */ /** A logger for events in [ResumeMediaBrowser]. */
@SysUISingleton @SysUISingleton
class ResumeMediaBrowserLogger @Inject constructor( class ResumeMediaBrowserLogger @Inject constructor(@MediaBrowserLog private val buffer: LogBuffer) {
@MediaBrowserLog private val buffer: LogBuffer
) {
/** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */ /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */
fun logConnection(componentName: ComponentName, reason: String) = buffer.log( fun logConnection(componentName: ComponentName, reason: String) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.DEBUG,
str1 = componentName.toShortString() {
str2 = reason str1 = componentName.toShortString()
}, str2 = reason
{ "Connecting browser for component $str1 due to $str2" } },
) { "Connecting browser for component $str1 due to $str2" }
)
/** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */ /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */
fun logDisconnect(componentName: ComponentName) = buffer.log( fun logDisconnect(componentName: ComponentName) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.DEBUG,
str1 = componentName.toShortString() { str1 = componentName.toShortString() },
}, { "Disconnecting browser for component $str1" }
{ "Disconnecting browser for component $str1" } )
)
/** /**
* Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed] * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed]
* event. * event.
* *
* @param isBrowserConnected true if there's a currently connected * @param isBrowserConnected true if there's a currently connected
* ```
* [android.media.browse.MediaBrowser] and false otherwise. * [android.media.browse.MediaBrowser] and false otherwise.
* @param componentName the component name for the [ResumeMediaBrowser] that triggered this log. * @param componentName
* ```
* the component name for the [ResumeMediaBrowser] that triggered this log.
*/ */
fun logSessionDestroyed( fun logSessionDestroyed(isBrowserConnected: Boolean, componentName: ComponentName) =
isBrowserConnected: Boolean, buffer.log(
componentName: ComponentName TAG,
) = buffer.log( LogLevel.DEBUG,
TAG, {
LogLevel.DEBUG, bool1 = isBrowserConnected
{ str1 = componentName.toShortString()
bool1 = isBrowserConnected },
str1 = componentName.toShortString() { "Session destroyed. Active browser = $bool1. Browser component = $str1." }
}, )
{ "Session destroyed. Active browser = $bool1. Browser component = $str1." }
)
} }
private const val TAG = "MediaBrowser" private const val TAG = "MediaBrowser"

View File

@@ -20,13 +20,17 @@ import android.graphics.drawable.Animatable2
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
/** /**
* AnimationBindHandler is responsible for tracking the bound animation state and preventing * AnimationBindHandler is responsible for tracking the bound animation state and preventing jank
* jank and conflicts due to media notifications arriving at any time during an animation. It * and conflicts due to media notifications arriving at any time during an animation. It does this
* does this in two parts. * in two parts.
* - Exit animations fired as a result of user input are tracked. When these are running, any * - Exit animations fired as a result of user input are tracked. When these are running, any
* ```
* bind actions are delayed until the animation completes (and then fired in sequence). * bind actions are delayed until the animation completes (and then fired in sequence).
* - Continuous animations are tracked using their rebind id. Later calls using the same * ```
* - Continuous animations are tracked using their rebind id. Later calls using the same
* ```
* rebind id will be totally ignored to prevent the continuous animation from restarting. * rebind id will be totally ignored to prevent the continuous animation from restarting.
* ```
*/ */
internal class AnimationBindHandler : Animatable2.AnimationCallback() { internal class AnimationBindHandler : Animatable2.AnimationCallback() {
private val onAnimationsComplete = mutableListOf<() -> Unit>() private val onAnimationsComplete = mutableListOf<() -> Unit>()
@@ -37,10 +41,10 @@ internal class AnimationBindHandler : Animatable2.AnimationCallback() {
get() = registrations.any { it.isRunning } get() = registrations.any { it.isRunning }
/** /**
* This check prevents rebinding to the action button if the identifier has not changed. A * This check prevents rebinding to the action button if the identifier has not changed. A null
* null value is always considered to be changed. This is used to prevent the connecting * value is always considered to be changed. This is used to prevent the connecting animation
* animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by * from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by an
* an application in a row. * application in a row.
*/ */
fun updateRebindId(newRebindId: Int?): Boolean { fun updateRebindId(newRebindId: Int?): Boolean {
if (rebindId == null || newRebindId == null || rebindId != newRebindId) { if (rebindId == null || newRebindId == null || rebindId != newRebindId) {
@@ -78,4 +82,4 @@ internal class AnimationBindHandler : Animatable2.AnimationCallback() {
onAnimationsComplete.clear() onAnimationsComplete.clear()
} }
} }
} }

View File

@@ -39,12 +39,11 @@ interface ColorTransition {
} }
/** /**
* A [ColorTransition] that animates between two specific colors. * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute
* It uses a ValueAnimator to execute the animation and interpolate between the source color and * the animation and interpolate between the source color and the target color.
* the target color.
* *
* Selection of the target color from the scheme, and application of the interpolated color * Selection of the target color from the scheme, and application of the interpolated color are
* are delegated to callbacks. * delegated to callbacks.
*/ */
open class AnimatingColorTransition( open class AnimatingColorTransition(
private val defaultColor: Int, private val defaultColor: Int,
@@ -59,9 +58,8 @@ open class AnimatingColorTransition(
var targetColor: Int = defaultColor var targetColor: Int = defaultColor
override fun onAnimationUpdate(animation: ValueAnimator) { override fun onAnimationUpdate(animation: ValueAnimator) {
currentColor = argbEvaluator.evaluate( currentColor =
animation.animatedFraction, sourceColor, targetColor argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int
) as Int
applyColor(currentColor) applyColor(currentColor)
} }
@@ -91,111 +89,126 @@ open class AnimatingColorTransition(
} }
typealias AnimatingColorTransitionFactory = typealias AnimatingColorTransitionFactory =
(Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
/** /**
* ColorSchemeTransition constructs a ColorTransition for each color in the scheme * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be
* that needs to be transitioned when changed. It also sets up the assignment functions for sending * transitioned when changed. It also sets up the assignment functions for sending the sending the
* the sending the interpolated colors to the appropriate views. * interpolated colors to the appropriate views.
*/ */
class ColorSchemeTransition internal constructor( class ColorSchemeTransition
internal constructor(
private val context: Context, private val context: Context,
private val mediaViewHolder: MediaViewHolder, private val mediaViewHolder: MediaViewHolder,
animatingColorTransitionFactory: AnimatingColorTransitionFactory animatingColorTransitionFactory: AnimatingColorTransitionFactory
) { ) {
constructor(context: Context, mediaViewHolder: MediaViewHolder) : constructor(
this(context, mediaViewHolder, ::AnimatingColorTransition) context: Context,
mediaViewHolder: MediaViewHolder
) : this(context, mediaViewHolder, ::AnimatingColorTransition)
val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
val surfaceColor = animatingColorTransitionFactory( val surfaceColor =
bgColor, animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor ->
::surfaceFromScheme val colorList = ColorStateList.valueOf(surfaceColor)
) { surfaceColor -> mediaViewHolder.seamlessIcon.imageTintList = colorList
val colorList = ColorStateList.valueOf(surfaceColor) mediaViewHolder.seamlessText.setTextColor(surfaceColor)
mediaViewHolder.seamlessIcon.imageTintList = colorList mediaViewHolder.albumView.backgroundTintList = colorList
mediaViewHolder.seamlessText.setTextColor(surfaceColor) mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
mediaViewHolder.albumView.backgroundTintList = colorList
mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
}
val accentPrimary = animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimary),
::accentPrimaryFromScheme
) { accentPrimary ->
val accentColorList = ColorStateList.valueOf(accentPrimary)
mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
}
val accentSecondary = animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimary),
::accentSecondaryFromScheme
) { accentSecondary ->
val colorList = ColorStateList.valueOf(accentSecondary)
(mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
it.setColor(colorList)
it.effectColor = colorList
} }
}
val colorSeamless = animatingColorTransitionFactory( val accentPrimary =
loadDefaultColor(R.attr.textColorPrimary), animatingColorTransitionFactory(
{ colorScheme: ColorScheme -> loadDefaultColor(R.attr.textColorPrimary),
// A1-100 dark in dark theme, A1-200 in light theme ::accentPrimaryFromScheme
if (context.resources.configuration.uiMode and ) { accentPrimary ->
Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) val accentColorList = ColorStateList.valueOf(accentPrimary)
colorScheme.accent1[2] mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
else colorScheme.accent1[3] mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
}, { seamlessColor: Int ->
val accentColorList = ColorStateList.valueOf(seamlessColor)
mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
})
val textPrimary = animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimary),
::textPrimaryFromScheme
) { textPrimary ->
mediaViewHolder.titleText.setTextColor(textPrimary)
val textColorList = ColorStateList.valueOf(textPrimary)
mediaViewHolder.seekBar.thumb.setTintList(textColorList)
mediaViewHolder.seekBar.progressTintList = textColorList
mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
for (button in mediaViewHolder.getTransparentActionButtons()) {
button.imageTintList = textColorList
} }
mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
}
val textPrimaryInverse = animatingColorTransitionFactory( val accentSecondary =
loadDefaultColor(R.attr.textColorPrimaryInverse), animatingColorTransitionFactory(
::textPrimaryInverseFromScheme loadDefaultColor(R.attr.textColorPrimary),
) { textPrimaryInverse -> ::accentSecondaryFromScheme
mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse) ) { accentSecondary ->
} val colorList = ColorStateList.valueOf(accentSecondary)
(mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
it.setColor(colorList)
it.effectColor = colorList
}
}
val textSecondary = animatingColorTransitionFactory( val colorSeamless =
loadDefaultColor(R.attr.textColorSecondary), animatingColorTransitionFactory(
::textSecondaryFromScheme loadDefaultColor(R.attr.textColorPrimary),
) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } { colorScheme: ColorScheme ->
// A1-100 dark in dark theme, A1-200 in light theme
if (
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
UI_MODE_NIGHT_YES
)
colorScheme.accent1[2]
else colorScheme.accent1[3]
},
{ seamlessColor: Int ->
val accentColorList = ColorStateList.valueOf(seamlessColor)
mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
}
)
val textTertiary = animatingColorTransitionFactory( val textPrimary =
loadDefaultColor(R.attr.textColorTertiary), animatingColorTransitionFactory(
::textTertiaryFromScheme loadDefaultColor(R.attr.textColorPrimary),
) { textTertiary -> ::textPrimaryFromScheme
mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary) ) { textPrimary ->
} mediaViewHolder.titleText.setTextColor(textPrimary)
val textColorList = ColorStateList.valueOf(textPrimary)
mediaViewHolder.seekBar.thumb.setTintList(textColorList)
mediaViewHolder.seekBar.progressTintList = textColorList
mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
for (button in mediaViewHolder.getTransparentActionButtons()) {
button.imageTintList = textColorList
}
mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
}
val colorTransitions = arrayOf( val textPrimaryInverse =
surfaceColor, animatingColorTransitionFactory(
colorSeamless, loadDefaultColor(R.attr.textColorPrimaryInverse),
accentPrimary, ::textPrimaryInverseFromScheme
accentSecondary, ) { textPrimaryInverse ->
textPrimary, mediaViewHolder.actionPlayPause.imageTintList =
textPrimaryInverse, ColorStateList.valueOf(textPrimaryInverse)
textSecondary, }
textTertiary,
) val textSecondary =
animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorSecondary),
::textSecondaryFromScheme
) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) }
val textTertiary =
animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorTertiary),
::textTertiaryFromScheme
) { textTertiary ->
mediaViewHolder.seekBar.progressBackgroundTintList =
ColorStateList.valueOf(textTertiary)
}
val colorTransitions =
arrayOf(
surfaceColor,
colorSeamless,
accentPrimary,
accentSecondary,
textPrimary,
textPrimaryInverse,
textSecondary,
textTertiary,
)
private fun loadDefaultColor(id: Int): Int { private fun loadDefaultColor(id: Int): Int {
return Utils.getColorAttr(context, id).defaultColor return Utils.getColorAttr(context, id).defaultColor

View File

@@ -42,22 +42,20 @@ import org.xmlpull.v1.XmlPullParser
private const val BACKGROUND_ANIM_DURATION = 370L private const val BACKGROUND_ANIM_DURATION = 370L
/** /** Drawable that can draw an animated gradient when tapped. */
* Drawable that can draw an animated gradient when tapped.
*/
@Keep @Keep
class IlluminationDrawable : Drawable() { class IlluminationDrawable : Drawable() {
private var themeAttrs: IntArray? = null private var themeAttrs: IntArray? = null
private var cornerRadiusOverride = -1f private var cornerRadiusOverride = -1f
var cornerRadius = 0f var cornerRadius = 0f
get() { get() {
return if (cornerRadiusOverride >= 0) { return if (cornerRadiusOverride >= 0) {
cornerRadiusOverride cornerRadiusOverride
} else { } else {
field field
}
} }
}
private var highlightColor = Color.TRANSPARENT private var highlightColor = Color.TRANSPARENT
private var tmpHsl = floatArrayOf(0f, 0f, 0f) private var tmpHsl = floatArrayOf(0f, 0f, 0f)
private var paint = Paint() private var paint = Paint()
@@ -65,22 +63,27 @@ class IlluminationDrawable : Drawable() {
private val lightSources = arrayListOf<LightSourceDrawable>() private val lightSources = arrayListOf<LightSourceDrawable>()
private var backgroundColor = Color.TRANSPARENT private var backgroundColor = Color.TRANSPARENT
set(value) { set(value) {
if (value == field) { if (value == field) {
return return
}
field = value
animateBackground()
} }
field = value
animateBackground()
}
private var backgroundAnimation: ValueAnimator? = null private var backgroundAnimation: ValueAnimator? = null
/** /** Draw background and gradient. */
* Draw background and gradient.
*/
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(), canvas.drawRoundRect(
cornerRadius, cornerRadius, paint) 0f,
0f,
bounds.width().toFloat(),
bounds.height().toFloat(),
cornerRadius,
cornerRadius,
paint
)
} }
override fun getOutline(outline: Outline) { override fun getOutline(outline: Outline) {
@@ -105,12 +108,11 @@ class IlluminationDrawable : Drawable() {
private fun updateStateFromTypedArray(a: TypedArray) { private fun updateStateFromTypedArray(a: TypedArray) {
if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) { if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) {
cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius =
cornerRadius) a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius)
} }
if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
100f
} }
} }
@@ -163,34 +165,42 @@ class IlluminationDrawable : Drawable() {
private fun animateBackground() { private fun animateBackground() {
ColorUtils.colorToHSL(backgroundColor, tmpHsl) ColorUtils.colorToHSL(backgroundColor, tmpHsl)
val L = tmpHsl[2] val L = tmpHsl[2]
tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) { tmpHsl[2] =
L + highlight MathUtils.constrain(
} else { if (L < 1f - highlight) {
L - highlight L + highlight
}, 0f, 1f) } else {
L - highlight
},
0f,
1f
)
val initialBackground = paint.color val initialBackground = paint.color
val initialHighlight = highlightColor val initialHighlight = highlightColor
val finalHighlight = ColorUtils.HSLToColor(tmpHsl) val finalHighlight = ColorUtils.HSLToColor(tmpHsl)
backgroundAnimation?.cancel() backgroundAnimation?.cancel()
backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply { backgroundAnimation =
duration = BACKGROUND_ANIM_DURATION ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = Interpolators.FAST_OUT_LINEAR_IN duration = BACKGROUND_ANIM_DURATION
addUpdateListener { interpolator = Interpolators.FAST_OUT_LINEAR_IN
val progress = it.animatedValue as Float addUpdateListener {
paint.color = blendARGB(initialBackground, backgroundColor, progress) val progress = it.animatedValue as Float
highlightColor = blendARGB(initialHighlight, finalHighlight, progress) paint.color = blendARGB(initialBackground, backgroundColor, progress)
lightSources.forEach { it.highlightColor = highlightColor } highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
invalidateSelf() lightSources.forEach { it.highlightColor = highlightColor }
} invalidateSelf()
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
backgroundAnimation = null
} }
}) addListener(
start() object : AnimatorListenerAdapter() {
} override fun onAnimationEnd(animation: Animator?) {
backgroundAnimation = null
}
}
)
start()
}
} }
override fun setTintList(tint: ColorStateList?) { override fun setTintList(tint: ColorStateList?) {
@@ -215,4 +225,4 @@ class IlluminationDrawable : Drawable() {
fun setCornerRadiusOverride(cornerRadius: Float?) { fun setCornerRadiusOverride(cornerRadius: Float?) {
cornerRadiusOverride = cornerRadius ?: -1f cornerRadiusOverride = cornerRadius ?: -1f
} }
} }

View File

@@ -45,7 +45,9 @@ import javax.inject.Named
* switches media player positioning between split pane container vs single pane container * switches media player positioning between split pane container vs single pane container
*/ */
@SysUISingleton @SysUISingleton
class KeyguardMediaController @Inject constructor( class KeyguardMediaController
@Inject
constructor(
@param:Named(KEYGUARD) private val mediaHost: MediaHost, @param:Named(KEYGUARD) private val mediaHost: MediaHost,
private val bypassController: KeyguardBypassController, private val bypassController: KeyguardBypassController,
private val statusBarStateController: SysuiStatusBarStateController, private val statusBarStateController: SysuiStatusBarStateController,
@@ -56,34 +58,40 @@ class KeyguardMediaController @Inject constructor(
) { ) {
init { init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener { statusBarStateController.addCallback(
override fun onStateChanged(newState: Int) { object : StatusBarStateController.StateListener {
refreshMediaPosition() override fun onStateChanged(newState: Int) {
}
})
configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
updateResources()
}
})
val settingsObserver: ContentObserver = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (uri == lockScreenMediaPlayerUri) {
allowMediaPlayerOnLockScreen =
secureSettings.getBoolForUser(
Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
true,
UserHandle.USER_CURRENT
)
refreshMediaPosition() refreshMediaPosition()
} }
} }
} )
configurationController.addCallback(
object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
updateResources()
}
}
)
val settingsObserver: ContentObserver =
object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (uri == lockScreenMediaPlayerUri) {
allowMediaPlayerOnLockScreen =
secureSettings.getBoolForUser(
Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
true,
UserHandle.USER_CURRENT
)
refreshMediaPosition()
}
}
}
secureSettings.registerContentObserverForUser( secureSettings.registerContentObserverForUser(
Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
settingsObserver, settingsObserver,
UserHandle.USER_ALL) UserHandle.USER_ALL
)
// First let's set the desired state that we want for this host // First let's set the desired state that we want for this host
mediaHost.expansion = MediaHostState.EXPANDED mediaHost.expansion = MediaHostState.EXPANDED
@@ -110,27 +118,21 @@ class KeyguardMediaController @Inject constructor(
refreshMediaPosition() refreshMediaPosition()
} }
/** /** Is the media player visible? */
* Is the media player visible?
*/
var visible = false var visible = false
private set private set
var visibilityChangedListener: ((Boolean) -> Unit)? = null var visibilityChangedListener: ((Boolean) -> Unit)? = null
/** /** single pane media container placed at the top of the notifications list */
* single pane media container placed at the top of the notifications list
*/
var singlePaneContainer: MediaContainerView? = null var singlePaneContainer: MediaContainerView? = null
private set private set
private var splitShadeContainer: ViewGroup? = null private var splitShadeContainer: ViewGroup? = null
/** /** Track the media player setting status on lock screen. */
* Track the media player setting status on lock screen.
*/
private var allowMediaPlayerOnLockScreen: Boolean = true private var allowMediaPlayerOnLockScreen: Boolean = true
private val lockScreenMediaPlayerUri = private val lockScreenMediaPlayerUri =
secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
/** /**
* Attaches media container in single pane mode, situated at the top of the notifications list * Attaches media container in single pane mode, situated at the top of the notifications list
@@ -146,9 +148,7 @@ class KeyguardMediaController @Inject constructor(
onMediaHostVisibilityChanged(mediaHost.visible) onMediaHostVisibilityChanged(mediaHost.visible)
} }
/** /** Called whenever the media hosts visibility changes */
* Called whenever the media hosts visibility changes
*/
private fun onMediaHostVisibilityChanged(visible: Boolean) { private fun onMediaHostVisibilityChanged(visible: Boolean) {
refreshMediaPosition() refreshMediaPosition()
if (visible) { if (visible) {
@@ -159,9 +159,7 @@ class KeyguardMediaController @Inject constructor(
} }
} }
/** /** Attaches media container in split shade mode, situated to the left of notifications */
* Attaches media container in split shade mode, situated to the left of notifications
*/
fun attachSplitShadeContainer(container: ViewGroup) { fun attachSplitShadeContainer(container: ViewGroup) {
splitShadeContainer = container splitShadeContainer = container
reattachHostView() reattachHostView()
@@ -183,9 +181,7 @@ class KeyguardMediaController @Inject constructor(
} }
if (activeContainer?.childCount == 0) { if (activeContainer?.childCount == 0) {
// Detach the hostView from its parent view if exists // Detach the hostView from its parent view if exists
mediaHost.hostView.parent?.let { mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
(it as? ViewGroup)?.removeView(mediaHost.hostView)
}
activeContainer.addView(mediaHost.hostView) activeContainer.addView(mediaHost.hostView)
} }
} }
@@ -193,7 +189,8 @@ class KeyguardMediaController @Inject constructor(
fun refreshMediaPosition() { fun refreshMediaPosition() {
val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD) val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
// mediaHost.visible required for proper animations handling // mediaHost.visible required for proper animations handling
visible = mediaHost.visible && visible =
mediaHost.visible &&
!bypassController.bypassEnabled && !bypassController.bypassEnabled &&
keyguardOrUserSwitcher && keyguardOrUserSwitcher &&
allowMediaPlayerOnLockScreen allowMediaPlayerOnLockScreen

View File

@@ -55,9 +55,7 @@ private data class RippleData(
var highlight: Float var highlight: Float
) )
/** /** Drawable that can draw an animated gradient when tapped. */
* Drawable that can draw an animated gradient when tapped.
*/
@Keep @Keep
class LightSourceDrawable : Drawable() { class LightSourceDrawable : Drawable() {
@@ -67,17 +65,15 @@ class LightSourceDrawable : Drawable() {
private var paint = Paint() private var paint = Paint()
var highlightColor = Color.WHITE var highlightColor = Color.WHITE
set(value) { set(value) {
if (field == value) { if (field == value) {
return return
}
field = value
invalidateSelf()
} }
field = value
invalidateSelf()
}
/** /** Draw a small highlight under the finger before expanding (or cancelling) it. */
* Draw a small highlight under the finger before expanding (or cancelling) it.
*/
private var active: Boolean = false private var active: Boolean = false
set(value) { set(value) {
if (value == field) { if (value == field) {
@@ -91,46 +87,54 @@ class LightSourceDrawable : Drawable() {
rippleData.progress = RIPPLE_DOWN_PROGRESS rippleData.progress = RIPPLE_DOWN_PROGRESS
} else { } else {
rippleAnimation?.cancel() rippleAnimation?.cancel()
rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply { rippleAnimation =
duration = RIPPLE_CANCEL_DURATION ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN duration = RIPPLE_CANCEL_DURATION
addUpdateListener { interpolator = Interpolators.LINEAR_OUT_SLOW_IN
rippleData.alpha = it.animatedValue as Float addUpdateListener {
invalidateSelf() rippleData.alpha = it.animatedValue as Float
}
addListener(object : AnimatorListenerAdapter() {
var cancelled = false
override fun onAnimationCancel(animation: Animator?) {
cancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
if (cancelled) {
return
}
rippleData.progress = 0f
rippleData.alpha = 0f
rippleAnimation = null
invalidateSelf() invalidateSelf()
} }
}) addListener(
start() object : AnimatorListenerAdapter() {
} var cancelled = false
override fun onAnimationCancel(animation: Animator?) {
cancelled = true
}
override fun onAnimationEnd(animation: Animator?) {
if (cancelled) {
return
}
rippleData.progress = 0f
rippleData.alpha = 0f
rippleAnimation = null
invalidateSelf()
}
}
)
start()
}
} }
invalidateSelf() invalidateSelf()
} }
private var rippleAnimation: Animator? = null private var rippleAnimation: Animator? = null
/** /** Draw background and gradient. */
* Draw background and gradient.
*/
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
val centerColor = val centerColor =
ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt()) ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
paint.shader = RadialGradient(rippleData.x, rippleData.y, radius, paint.shader =
intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP) RadialGradient(
rippleData.x,
rippleData.y,
radius,
intArrayOf(centerColor, Color.TRANSPARENT),
GRADIENT_STOPS,
Shader.TileMode.CLAMP
)
canvas.drawCircle(rippleData.x, rippleData.y, radius, paint) canvas.drawCircle(rippleData.x, rippleData.y, radius, paint)
} }
@@ -162,8 +166,8 @@ class LightSourceDrawable : Drawable() {
rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f) rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f)
} }
if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) { if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / rippleData.highlight =
100f a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
} }
} }
@@ -193,40 +197,44 @@ class LightSourceDrawable : Drawable() {
invalidateSelf() invalidateSelf()
} }
/** /** Draws an animated ripple that expands fading away. */
* Draws an animated ripple that expands fading away.
*/
private fun illuminate() { private fun illuminate() {
rippleData.alpha = 1f rippleData.alpha = 1f
invalidateSelf() invalidateSelf()
rippleAnimation?.cancel() rippleAnimation?.cancel()
rippleAnimation = AnimatorSet().apply { rippleAnimation =
playTogether(ValueAnimator.ofFloat(1f, 0f).apply { AnimatorSet().apply {
startDelay = 133 playTogether(
duration = RIPPLE_ANIM_DURATION - startDelay ValueAnimator.ofFloat(1f, 0f).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN startDelay = 133
addUpdateListener { duration = RIPPLE_ANIM_DURATION - startDelay
rippleData.alpha = it.animatedValue as Float interpolator = Interpolators.LINEAR_OUT_SLOW_IN
invalidateSelf() addUpdateListener {
} rippleData.alpha = it.animatedValue as Float
}, ValueAnimator.ofFloat(rippleData.progress, 1f).apply { invalidateSelf()
duration = RIPPLE_ANIM_DURATION }
interpolator = Interpolators.LINEAR_OUT_SLOW_IN },
addUpdateListener { ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
rippleData.progress = it.animatedValue as Float duration = RIPPLE_ANIM_DURATION
invalidateSelf() interpolator = Interpolators.LINEAR_OUT_SLOW_IN
} addUpdateListener {
}) rippleData.progress = it.animatedValue as Float
addListener(object : AnimatorListenerAdapter() { invalidateSelf()
override fun onAnimationEnd(animation: Animator?) { }
rippleData.progress = 0f }
rippleAnimation = null )
invalidateSelf() addListener(
} object : AnimatorListenerAdapter() {
}) override fun onAnimationEnd(animation: Animator?) {
start() rippleData.progress = 0f
} rippleAnimation = null
invalidateSelf()
}
}
)
start()
}
} }
override fun setHotspot(x: Float, y: Float) { override fun setHotspot(x: Float, y: Float) {
@@ -251,8 +259,13 @@ class LightSourceDrawable : Drawable() {
override fun getDirtyBounds(): Rect { override fun getDirtyBounds(): Rect {
val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress) val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(), val bounds =
(rippleData.x + radius).toInt(), (rippleData.y + radius).toInt()) Rect(
(rippleData.x - radius).toInt(),
(rippleData.y - radius).toInt(),
(rippleData.x + radius).toInt(),
(rippleData.y + radius).toInt()
)
bounds.union(super.getDirtyBounds()) bounds.union(super.getDirtyBounds())
return bounds return bounds
} }
@@ -293,4 +306,4 @@ class LightSourceDrawable : Drawable() {
return changed return changed
} }
} }

View File

@@ -24,53 +24,43 @@ import javax.inject.Inject
/** A debug logger for [MediaCarouselController]. */ /** A debug logger for [MediaCarouselController]. */
@SysUISingleton @SysUISingleton
class MediaCarouselControllerLogger @Inject constructor( class MediaCarouselControllerLogger
@MediaCarouselControllerLog private val buffer: LogBuffer @Inject
) { constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) {
/** /**
* Log that there might be a potential memory leak for the [MediaControlPanel] and/or * Log that there might be a potential memory leak for the [MediaControlPanel] and/or
* [MediaViewController] related to [key]. * [MediaViewController] related to [key].
*/ */
fun logPotentialMemoryLeak(key: String) = buffer.log( fun logPotentialMemoryLeak(key: String) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ str1 = key }, LogLevel.DEBUG,
{ { str1 = key },
"Potential memory leak: " + {
"Potential memory leak: " +
"Removing control panel for $str1 from map without calling #onDestroy" "Removing control panel for $str1 from map without calling #onDestroy"
} }
) )
fun logMediaLoaded(key: String) = buffer.log( fun logMediaLoaded(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add player $str1" })
LogLevel.DEBUG,
{ str1 = key },
{ "add player $str1" }
)
fun logMediaRemoved(key: String) = buffer.log( fun logMediaRemoved(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
LogLevel.DEBUG,
{ str1 = key },
{ "removing player $str1" }
)
fun logRecommendationLoaded(key: String) = buffer.log( fun logRecommendationLoaded(key: String) =
TAG, buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
LogLevel.DEBUG,
{ str1 = key },
{ "add recommendation $str1" }
)
fun logRecommendationRemoved(key: String, immediately: Boolean) = buffer.log( fun logRecommendationRemoved(key: String, immediately: Boolean) =
TAG, buffer.log(
LogLevel.DEBUG, TAG,
{ LogLevel.DEBUG,
str1 = key {
bool1 = immediately str1 = key
}, bool1 = immediately
{ "removing recommendation $str1, immediate=$bool1" } },
) { "removing recommendation $str1, immediate=$bool1" }
)
} }
private const val TAG = "MediaCarouselCtlrLog" private const val TAG = "MediaCarouselCtlrLog"

View File

@@ -44,16 +44,13 @@ private const val RUBBERBAND_FACTOR = 0.2f
private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
/** /**
* Default spring configuration to use for animations where stiffness and/or damping ratio * Default spring configuration to use for animations where stiffness and/or damping ratio were not
* were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. * provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
*/ */
private val translationConfig = PhysicsAnimator.SpringConfig( private val translationConfig =
SpringForce.STIFFNESS_LOW, PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
SpringForce.DAMPING_RATIO_LOW_BOUNCY)
/** /** A controller class for the media scrollview, responsible for touch handling */
* A controller class for the media scrollview, responsible for touch handling
*/
class MediaCarouselScrollHandler( class MediaCarouselScrollHandler(
private val scrollView: MediaScrollView, private val scrollView: MediaScrollView,
private val pageIndicator: PageIndicator, private val pageIndicator: PageIndicator,
@@ -66,57 +63,36 @@ class MediaCarouselScrollHandler(
private val logSmartspaceImpression: (Boolean) -> Unit, private val logSmartspaceImpression: (Boolean) -> Unit,
private val logger: MediaUiEventLogger private val logger: MediaUiEventLogger
) { ) {
/** /** Is the view in RTL */
* Is the view in RTL val isRtl: Boolean
*/ get() = scrollView.isLayoutRtl
val isRtl: Boolean get() = scrollView.isLayoutRtl /** Do we need falsing protection? */
/**
* Do we need falsing protection?
*/
var falsingProtectionNeeded: Boolean = false var falsingProtectionNeeded: Boolean = false
/** /** The width of the carousel */
* The width of the carousel
*/
private var carouselWidth: Int = 0 private var carouselWidth: Int = 0
/** /** The height of the carousel */
* The height of the carousel
*/
private var carouselHeight: Int = 0 private var carouselHeight: Int = 0
/** /** How much are we scrolled into the current media? */
* How much are we scrolled into the current media?
*/
private var cornerRadius: Int = 0 private var cornerRadius: Int = 0
/** /** The content where the players are added */
* The content where the players are added
*/
private var mediaContent: ViewGroup private var mediaContent: ViewGroup
/** /** The gesture detector to detect touch gestures */
* The gesture detector to detect touch gestures
*/
private val gestureDetector: GestureDetectorCompat private val gestureDetector: GestureDetectorCompat
/** /** The settings button view */
* The settings button view
*/
private lateinit var settingsButton: View private lateinit var settingsButton: View
/** /** What's the currently visible player index? */
* What's the currently visible player index?
*/
var visibleMediaIndex: Int = 0 var visibleMediaIndex: Int = 0
private set private set
/** /** How much are we scrolled into the current media? */
* How much are we scrolled into the current media?
*/
private var scrollIntoCurrentMedia: Int = 0 private var scrollIntoCurrentMedia: Int = 0
/** /** how much is the content translated in X */
* how much is the content translated in X
*/
var contentTranslation = 0.0f var contentTranslation = 0.0f
private set(value) { private set(value) {
field = value field = value
@@ -126,9 +102,7 @@ class MediaCarouselScrollHandler(
updateClipToOutline() updateClipToOutline()
} }
/** /** The width of a player including padding */
* The width of a player including padding
*/
var playerWidthPlusPadding: Int = 0 var playerWidthPlusPadding: Int = 0
set(value) { set(value) {
field = value field = value
@@ -136,82 +110,75 @@ class MediaCarouselScrollHandler(
// it's still at the same place // it's still at the same place
var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding
if (scrollIntoCurrentMedia > playerWidthPlusPadding) { if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
newRelativeScroll += playerWidthPlusPadding - newRelativeScroll +=
(scrollIntoCurrentMedia - playerWidthPlusPadding) playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding)
} else { } else {
newRelativeScroll += scrollIntoCurrentMedia newRelativeScroll += scrollIntoCurrentMedia
} }
scrollView.relativeScrollX = newRelativeScroll scrollView.relativeScrollX = newRelativeScroll
} }
/** /** Does the dismiss currently show the setting cog? */
* Does the dismiss currently show the setting cog?
*/
var showsSettingsButton: Boolean = false var showsSettingsButton: Boolean = false
/** /** A utility to detect gestures, used in the touch listener */
* A utility to detect gestures, used in the touch listener private val gestureListener =
*/ object : GestureDetector.SimpleOnGestureListener() {
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { override fun onFling(
override fun onFling( eStart: MotionEvent?,
eStart: MotionEvent?, eCurrent: MotionEvent?,
eCurrent: MotionEvent?, vX: Float,
vX: Float, vY: Float
vY: Float ) = onFling(vX, vY)
) = onFling(vX, vY)
override fun onScroll( override fun onScroll(
down: MotionEvent?, down: MotionEvent?,
lastMotion: MotionEvent?, lastMotion: MotionEvent?,
distanceX: Float, distanceX: Float,
distanceY: Float distanceY: Float
) = onScroll(down!!, lastMotion!!, distanceX) ) = onScroll(down!!, lastMotion!!, distanceX)
override fun onDown(e: MotionEvent?): Boolean { override fun onDown(e: MotionEvent?): Boolean {
if (falsingProtectionNeeded) { if (falsingProtectionNeeded) {
falsingCollector.onNotificationStartDismissing() falsingCollector.onNotificationStartDismissing()
}
return false
} }
return false
} }
}
/** /** The touch listener for the scroll view */
* The touch listener for the scroll view private val touchListener =
*/ object : Gefingerpoken {
private val touchListener = object : Gefingerpoken { override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) }
}
/** /** A listener that is invoked when the scrolling changes to update player visibilities */
* A listener that is invoked when the scrolling changes to update player visibilities private val scrollChangedListener =
*/ object : View.OnScrollChangeListener {
private val scrollChangedListener = object : View.OnScrollChangeListener { override fun onScrollChange(
override fun onScrollChange( v: View?,
v: View?, scrollX: Int,
scrollX: Int, scrollY: Int,
scrollY: Int, oldScrollX: Int,
oldScrollX: Int, oldScrollY: Int
oldScrollY: Int ) {
) { if (playerWidthPlusPadding == 0) {
if (playerWidthPlusPadding == 0) { return
return }
val relativeScrollX = scrollView.relativeScrollX
onMediaScrollingChanged(
relativeScrollX / playerWidthPlusPadding,
relativeScrollX % playerWidthPlusPadding
)
} }
val relativeScrollX = scrollView.relativeScrollX
onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding,
relativeScrollX % playerWidthPlusPadding)
} }
}
/** /** Whether the media card is visible to user if any */
* Whether the media card is visible to user if any
*/
var visibleToUser: Boolean = false var visibleToUser: Boolean = false
/** /** Whether the quick setting is expanded or not */
* Whether the quick setting is expanded or not
*/
var qsExpanded: Boolean = false var qsExpanded: Boolean = false
init { init {
@@ -220,47 +187,61 @@ class MediaCarouselScrollHandler(
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER) scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
mediaContent = scrollView.contentContainer mediaContent = scrollView.contentContainer
scrollView.setOnScrollChangeListener(scrollChangedListener) scrollView.setOnScrollChangeListener(scrollChangedListener)
scrollView.outlineProvider = object : ViewOutlineProvider() { scrollView.outlineProvider =
override fun getOutline(view: View?, outline: Outline?) { object : ViewOutlineProvider() {
outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat()) override fun getOutline(view: View?, outline: Outline?) {
outline?.setRoundRect(
0,
0,
carouselWidth,
carouselHeight,
cornerRadius.toFloat()
)
}
} }
}
} }
fun onSettingsButtonUpdated(button: View) { fun onSettingsButtonUpdated(button: View) {
settingsButton = button settingsButton = button
// We don't have a context to resolve, lets use the settingsbuttons one since that is // We don't have a context to resolve, lets use the settingsbuttons one since that is
// reinflated appropriately // reinflated appropriately
cornerRadius = settingsButton.resources.getDimensionPixelSize( cornerRadius =
Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)) settingsButton.resources.getDimensionPixelSize(
Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)
)
updateSettingsPresentation() updateSettingsPresentation()
scrollView.invalidateOutline() scrollView.invalidateOutline()
} }
private fun updateSettingsPresentation() { private fun updateSettingsPresentation() {
if (showsSettingsButton && settingsButton.width > 0) { if (showsSettingsButton && settingsButton.width > 0) {
val settingsOffset = MathUtils.map( val settingsOffset =
MathUtils.map(
0.0f, 0.0f,
getMaxTranslation().toFloat(), getMaxTranslation().toFloat(),
0.0f, 0.0f,
1.0f, 1.0f,
Math.abs(contentTranslation)) Math.abs(contentTranslation)
val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * )
val settingsTranslation =
(1.0f - settingsOffset) *
-settingsButton.width *
SETTINGS_BUTTON_TRANSLATION_FRACTION SETTINGS_BUTTON_TRANSLATION_FRACTION
val newTranslationX = if (isRtl) { val newTranslationX =
// In RTL, the 0-placement is on the right side of the view, not the left... if (isRtl) {
if (contentTranslation > 0) { // In RTL, the 0-placement is on the right side of the view, not the left...
-(scrollView.width - settingsTranslation - settingsButton.width) if (contentTranslation > 0) {
-(scrollView.width - settingsTranslation - settingsButton.width)
} else {
-settingsTranslation
}
} else { } else {
-settingsTranslation if (contentTranslation > 0) {
settingsTranslation
} else {
scrollView.width - settingsTranslation - settingsButton.width
}
} }
} else {
if (contentTranslation > 0) {
settingsTranslation
} else {
scrollView.width - settingsTranslation - settingsButton.width
}
}
val rotation = (1.0f - settingsOffset) * 50 val rotation = (1.0f - settingsOffset) * 50
settingsButton.rotation = rotation * -Math.signum(contentTranslation) settingsButton.rotation = rotation * -Math.signum(contentTranslation)
val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset)) val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset))
@@ -307,16 +288,14 @@ class MediaCarouselScrollHandler(
val newScrollX = scrollView.relativeScrollX + dx val newScrollX = scrollView.relativeScrollX + dx
// Delay the scrolling since scrollView calls springback which cancels // Delay the scrolling since scrollView calls springback which cancels
// the animation again.. // the animation again..
mainExecutor.execute { mainExecutor.execute { scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) }
scrollView.smoothScrollTo(newScrollX, scrollView.scrollY)
}
} }
val currentTranslation = scrollView.getContentTranslation() val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f) { if (currentTranslation != 0.0f) {
// We started a Swipe but didn't end up with a fling. Let's either go to the // We started a Swipe but didn't end up with a fling. Let's either go to the
// dismissed position or go back. // dismissed position or go back.
val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 || val springBack =
isFalseTouch() Math.abs(currentTranslation) < getMaxTranslation() / 2 || isFalseTouch()
val newTranslation: Float val newTranslation: Float
if (springBack) { if (springBack) {
newTranslation = 0.0f newTranslation = 0.0f
@@ -325,13 +304,17 @@ class MediaCarouselScrollHandler(
if (!showsSettingsButton) { if (!showsSettingsButton) {
// Delay the dismiss a bit to avoid too much overlap. Waiting until the // Delay the dismiss a bit to avoid too much overlap. Waiting until the
// animation has finished also feels a bit too slow here. // animation has finished also feels a bit too slow here.
mainExecutor.executeDelayed({ mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY)
dismissCallback.invoke()
}, DISMISS_DELAY)
} }
} }
PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, PhysicsAnimator.getInstance(this)
newTranslation, startVelocity = 0.0f, config = translationConfig).start() .spring(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = 0.0f,
config = translationConfig
)
.start()
scrollView.animationTargetX = newTranslation scrollView.animationTargetX = newTranslation
} }
} }
@@ -339,10 +322,11 @@ class MediaCarouselScrollHandler(
return false return false
} }
private fun isFalseTouch() = falsingProtectionNeeded && private fun isFalseTouch() =
falsingManager.isFalseTouch(NOTIFICATION_DISMISS) falsingProtectionNeeded && falsingManager.isFalseTouch(NOTIFICATION_DISMISS)
private fun getMaxTranslation() = if (showsSettingsButton) { private fun getMaxTranslation() =
if (showsSettingsButton) {
settingsButton.width settingsButton.width
} else { } else {
playerWidthPlusPadding playerWidthPlusPadding
@@ -352,15 +336,10 @@ class MediaCarouselScrollHandler(
return gestureDetector.onTouchEvent(motionEvent) return gestureDetector.onTouchEvent(motionEvent)
} }
fun onScroll( fun onScroll(down: MotionEvent, lastMotion: MotionEvent, distanceX: Float): Boolean {
down: MotionEvent,
lastMotion: MotionEvent,
distanceX: Float
): Boolean {
val totalX = lastMotion.x - down.x val totalX = lastMotion.x - down.x
val currentTranslation = scrollView.getContentTranslation() val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f || if (currentTranslation != 0.0f || !scrollView.canScrollHorizontally((-totalX).toInt())) {
!scrollView.canScrollHorizontally((-totalX).toInt())) {
var newTranslation = currentTranslation - distanceX var newTranslation = currentTranslation - distanceX
val absTranslation = Math.abs(newTranslation) val absTranslation = Math.abs(newTranslation)
if (absTranslation > getMaxTranslation()) { if (absTranslation > getMaxTranslation()) {
@@ -374,14 +353,18 @@ class MediaCarouselScrollHandler(
newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR
} else { } else {
// We just crossed the boundary, let's rubberband it all // We just crossed the boundary, let's rubberband it all
newTranslation = Math.signum(newTranslation) * (getMaxTranslation() + newTranslation =
(absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR) Math.signum(newTranslation) *
(getMaxTranslation() +
(absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
} }
} // Otherwise we don't have do do anything, and will remove the unrubberbanded } // Otherwise we don't have do do anything, and will remove the unrubberbanded
// translation // translation
} }
if (Math.signum(newTranslation) != Math.signum(currentTranslation) && if (
currentTranslation != 0.0f) { Math.signum(newTranslation) != Math.signum(currentTranslation) &&
currentTranslation != 0.0f
) {
// We crossed the 0.0 threshold of the translation. Let's see if we're allowed // We crossed the 0.0 threshold of the translation. Let's see if we're allowed
// to scroll into the new direction // to scroll into the new direction
if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
@@ -392,8 +375,14 @@ class MediaCarouselScrollHandler(
} }
val physicsAnimator = PhysicsAnimator.getInstance(this) val physicsAnimator = PhysicsAnimator.getInstance(this)
if (physicsAnimator.isRunning()) { if (physicsAnimator.isRunning()) {
physicsAnimator.spring(CONTENT_TRANSLATION, physicsAnimator
newTranslation, startVelocity = 0.0f, config = translationConfig).start() .spring(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = 0.0f,
config = translationConfig
)
.start()
} else { } else {
contentTranslation = newTranslation contentTranslation = newTranslation
} }
@@ -403,10 +392,7 @@ class MediaCarouselScrollHandler(
return false return false
} }
private fun onFling( private fun onFling(vX: Float, vY: Float): Boolean {
vX: Float,
vY: Float
): Boolean {
if (vX * vX < 0.5 * vY * vY) { if (vX * vX < 0.5 * vY * vY) {
return false return false
} }
@@ -425,13 +411,17 @@ class MediaCarouselScrollHandler(
// Delay the dismiss a bit to avoid too much overlap. Waiting until the animation // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation
// has finished also feels a bit too slow here. // has finished also feels a bit too slow here.
if (!showsSettingsButton) { if (!showsSettingsButton) {
mainExecutor.executeDelayed({ mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY)
dismissCallback.invoke()
}, DISMISS_DELAY)
} }
} }
PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, PhysicsAnimator.getInstance(this)
newTranslation, startVelocity = vX, config = translationConfig).start() .spring(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = vX,
config = translationConfig
)
.start()
scrollView.animationTargetX = newTranslation scrollView.animationTargetX = newTranslation
} else { } else {
// We're flinging the player! Let's go either to the previous or to the next player // We're flinging the player! Let's go either to the previous or to the next player
@@ -444,21 +434,18 @@ class MediaCarouselScrollHandler(
val view = mediaContent.getChildAt(destIndex) val view = mediaContent.getChildAt(destIndex)
// We need to post this since we're dispatching a touch to the underlying view to cancel // We need to post this since we're dispatching a touch to the underlying view to cancel
// but canceling will actually abort the animation. // but canceling will actually abort the animation.
mainExecutor.execute { mainExecutor.execute { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }
scrollView.smoothScrollTo(view.left, scrollView.scrollY)
}
} }
return true return true
} }
/** /** Reset the translation of the players when swiped */
* Reset the translation of the players when swiped
*/
fun resetTranslation(animate: Boolean = false) { fun resetTranslation(animate: Boolean = false) {
if (scrollView.getContentTranslation() != 0.0f) { if (scrollView.getContentTranslation() != 0.0f) {
if (animate) { if (animate) {
PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, PhysicsAnimator.getInstance(this)
0.0f, config = translationConfig).start() .spring(CONTENT_TRANSLATION, 0.0f, config = translationConfig)
.start()
scrollView.animationTargetX = 0.0f scrollView.animationTargetX = 0.0f
} else { } else {
PhysicsAnimator.getInstance(this).cancel() PhysicsAnimator.getInstance(this).cancel()
@@ -486,21 +473,22 @@ class MediaCarouselScrollHandler(
closeGuts(false) closeGuts(false)
updatePlayerVisibilities() updatePlayerVisibilities()
} }
val relativeLocation = visibleMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) val relativeLocation =
scrollInAmount.toFloat() / playerWidthPlusPadding else 0f visibleMediaIndex.toFloat() +
if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding
else 0f
// Fix the location, because PageIndicator does not handle RTL internally // Fix the location, because PageIndicator does not handle RTL internally
val location = if (isRtl) { val location =
mediaContent.childCount - relativeLocation - 1 if (isRtl) {
} else { mediaContent.childCount - relativeLocation - 1
relativeLocation } else {
} relativeLocation
}
pageIndicator.setLocation(location) pageIndicator.setLocation(location)
updateClipToOutline() updateClipToOutline()
} }
/** /** Notified whenever the players or their order has changed */
* Notified whenever the players or their order has changed
*/
fun onPlayersChanged() { fun onPlayersChanged() {
updatePlayerVisibilities() updatePlayerVisibilities()
updateMediaPaddings() updateMediaPaddings()
@@ -530,8 +518,8 @@ class MediaCarouselScrollHandler(
} }
/** /**
* Notify that a player will be removed right away. This gives us the opporunity to look * Notify that a player will be removed right away. This gives us the opporunity to look where
* where it was and update our scroll position. * it was and update our scroll position.
*/ */
fun onPrePlayerRemoved(removed: MediaControlPanel) { fun onPrePlayerRemoved(removed: MediaControlPanel) {
val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player) val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player)
@@ -551,9 +539,7 @@ class MediaCarouselScrollHandler(
} }
} }
/** /** Update the bounds of the carousel */
* Update the bounds of the carousel
*/
fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) { fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) {
if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) { if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) {
carouselWidth = currentCarouselWidth carouselWidth = currentCarouselWidth
@@ -562,9 +548,7 @@ class MediaCarouselScrollHandler(
} }
} }
/** /** Reset the MediaScrollView to the start. */
* Reset the MediaScrollView to the start.
*/
fun scrollToStart() { fun scrollToStart() {
scrollView.relativeScrollX = 0 scrollView.relativeScrollX = 0
} }
@@ -582,21 +566,22 @@ class MediaCarouselScrollHandler(
val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
val view = mediaContent.getChildAt(destIndex) val view = mediaContent.getChildAt(destIndex)
// We need to post this to wait for the active player becomes visible. // We need to post this to wait for the active player becomes visible.
mainExecutor.executeDelayed({ mainExecutor.executeDelayed(
scrollView.smoothScrollTo(view.left, scrollView.scrollY) { scrollView.smoothScrollTo(view.left, scrollView.scrollY) },
}, SCROLL_DELAY) SCROLL_DELAY
)
} }
companion object { companion object {
private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( private val CONTENT_TRANSLATION =
"contentTranslation") { object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") {
override fun getValue(handler: MediaCarouselScrollHandler): Float { override fun getValue(handler: MediaCarouselScrollHandler): Float {
return handler.contentTranslation return handler.contentTranslation
} }
override fun setValue(handler: MediaCarouselScrollHandler, value: Float) { override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
handler.contentTranslation = value handler.contentTranslation = value
}
} }
}
} }
} }

View File

@@ -30,7 +30,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView
import java.util.Objects import java.util.Objects
import javax.inject.Inject import javax.inject.Inject
class MediaHost constructor( class MediaHost
constructor(
private val state: MediaHostStateHolder, private val state: MediaHostStateHolder,
private val mediaHierarchyManager: MediaHierarchyManager, private val mediaHierarchyManager: MediaHierarchyManager,
private val mediaDataManager: MediaDataManager, private val mediaDataManager: MediaDataManager,
@@ -45,14 +46,10 @@ class MediaHost constructor(
private var inited: Boolean = false private var inited: Boolean = false
/** /** Are we listening to media data changes? */
* Are we listening to media data changes?
*/
private var listeningToMediaData = false private var listeningToMediaData = false
/** /** Get the current bounds on the screen. This makes sure the state is fresh and up to date */
* Get the current bounds on the screen. This makes sure the state is fresh and up to date
*/
val currentBounds: Rect = Rect() val currentBounds: Rect = Rect()
get() { get() {
hostView.getLocationOnScreen(tmpLocationOnScreen) hostView.getLocationOnScreen(tmpLocationOnScreen)
@@ -81,38 +78,39 @@ class MediaHost constructor(
*/ */
val currentClipping = Rect() val currentClipping = Rect()
private val listener = object : MediaDataManager.Listener { private val listener =
override fun onMediaDataLoaded( object : MediaDataManager.Listener {
key: String, override fun onMediaDataLoaded(
oldKey: String?, key: String,
data: MediaData, oldKey: String?,
immediately: Boolean, data: MediaData,
receivedSmartspaceCardLatency: Int, immediately: Boolean,
isSsReactivated: Boolean receivedSmartspaceCardLatency: Int,
) { isSsReactivated: Boolean
if (immediately) { ) {
if (immediately) {
updateViewVisibility()
}
}
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
updateViewVisibility() updateViewVisibility()
} }
}
override fun onSmartspaceMediaDataLoaded( override fun onMediaDataRemoved(key: String) {
key: String,
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
updateViewVisibility()
}
override fun onMediaDataRemoved(key: String) {
updateViewVisibility()
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
if (immediately) {
updateViewVisibility() updateViewVisibility()
} }
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
if (immediately) {
updateViewVisibility()
}
}
} }
}
fun addVisibilityChangeListener(listener: (Boolean) -> Unit) { fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
visibleChangedListeners.add(listener) visibleChangedListeners.add(listener)
@@ -123,12 +121,14 @@ class MediaHost constructor(
} }
/** /**
* Initialize this MediaObject and create a host view. * Initialize this MediaObject and create a host view. All state should already be set on this
* All state should already be set on this host before calling this method in order to avoid * host before calling this method in order to avoid unnecessary state changes which lead to
* unnecessary state changes which lead to remeasurings later on. * remeasurings later on.
* *
* @param location the location this host name has. Used to identify the host during * @param location the location this host name has. Used to identify the host during
* ```
* transitions. * transitions.
* ```
*/ */
fun init(@MediaLocation location: Int) { fun init(@MediaLocation location: Int) {
if (inited) { if (inited) {
@@ -141,36 +141,42 @@ class MediaHost constructor(
// Listen by default, as the host might not be attached by our clients, until // Listen by default, as the host might not be attached by our clients, until
// they get a visibility change. We still want to stay up to date in that case! // they get a visibility change. We still want to stay up to date in that case!
setListeningToMediaData(true) setListeningToMediaData(true)
hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { hostView.addOnAttachStateChangeListener(
override fun onViewAttachedToWindow(v: View?) { object : OnAttachStateChangeListener {
setListeningToMediaData(true) override fun onViewAttachedToWindow(v: View?) {
updateViewVisibility() setListeningToMediaData(true)
} updateViewVisibility()
}
override fun onViewDetachedFromWindow(v: View?) { override fun onViewDetachedFromWindow(v: View?) {
setListeningToMediaData(false) setListeningToMediaData(false)
}
} }
}) )
// Listen to measurement updates and update our state with it // Listen to measurement updates and update our state with it
hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager { hostView.measurementManager =
override fun onMeasure(input: MeasurementInput): MeasurementOutput { object : UniqueObjectHostView.MeasurementManager {
// Modify the measurement to exactly match the dimensions override fun onMeasure(input: MeasurementInput): MeasurementOutput {
if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) { // Modify the measurement to exactly match the dimensions
input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( if (
View.MeasureSpec.getSize(input.widthMeasureSpec), View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST
View.MeasureSpec.EXACTLY) ) {
input.widthMeasureSpec =
View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(input.widthMeasureSpec),
View.MeasureSpec.EXACTLY
)
}
// This will trigger a state change that ensures that we now have a state
// available
state.measurementInput = input
return mediaHostStatesManager.updateCarouselDimensions(location, state)
} }
// This will trigger a state change that ensures that we now have a state available
state.measurementInput = input
return mediaHostStatesManager.updateCarouselDimensions(location, state)
} }
}
// Whenever the state changes, let our state manager know // Whenever the state changes, let our state manager know
state.changedListener = { state.changedListener = { mediaHostStatesManager.updateHostState(location, state) }
mediaHostStatesManager.updateHostState(location, state)
}
updateViewVisibility() updateViewVisibility()
} }
@@ -191,17 +197,16 @@ class MediaHost constructor(
* the visibility has changed * the visibility has changed
*/ */
fun updateViewVisibility() { fun updateViewVisibility() {
state.visible = if (showsOnlyActiveMedia) { state.visible =
mediaDataManager.hasActiveMediaOrRecommendation() if (showsOnlyActiveMedia) {
} else { mediaDataManager.hasActiveMediaOrRecommendation()
mediaDataManager.hasAnyMediaOrRecommendation() } else {
} mediaDataManager.hasAnyMediaOrRecommendation()
}
val newVisibility = if (visible) View.VISIBLE else View.GONE val newVisibility = if (visible) View.VISIBLE else View.GONE
if (newVisibility != hostView.visibility) { if (newVisibility != hostView.visibility) {
hostView.visibility = newVisibility hostView.visibility = newVisibility
visibleChangedListeners.forEach { visibleChangedListeners.forEach { it.invoke(visible) }
it.invoke(visible)
}
} }
} }
@@ -269,14 +274,10 @@ class MediaHost constructor(
private var lastDisappearHash = disappearParameters.hashCode() private var lastDisappearHash = disappearParameters.hashCode()
/** /** A listener for all changes. This won't be copied over when invoking [copy] */
* A listener for all changes. This won't be copied over when invoking [copy]
*/
var changedListener: (() -> Unit)? = null var changedListener: (() -> Unit)? = null
/** /** Get a copy of this state. This won't copy any listeners it may have set */
* Get a copy of this state. This won't copy any listeners it may have set
*/
override fun copy(): MediaHostState { override fun copy(): MediaHostState {
val mediaHostState = MediaHostStateHolder() val mediaHostState = MediaHostStateHolder()
mediaHostState.expansion = expansion mediaHostState.expansion = expansion
@@ -331,15 +332,13 @@ class MediaHost constructor(
} }
/** /**
* A description of a media host state that describes the behavior whenever the media carousel * A description of a media host state that describes the behavior whenever the media carousel is
* is hosted. The HostState notifies the media players of changes to their properties, who * hosted. The HostState notifies the media players of changes to their properties, who in turn will
* in turn will create view states from it. * create view states from it. When adding a new property to this, make sure to update the listener
* When adding a new property to this, make sure to update the listener and notify them * and notify them about the changes. In case you need to have a different rendering based on the
* about the changes. * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host
* In case you need to have a different rendering based on the state, you can add a new * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure
* constraintState to the [MediaViewController]. Otherwise, similar host states will resolve * to only update that key if the underlying view needs to have a different measurement.
* to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update
* that key if the underlying view needs to have a different measurement.
*/ */
interface MediaHostState { interface MediaHostState {
@@ -349,46 +348,36 @@ interface MediaHostState {
} }
/** /**
* The last measurement input that this state was measured with. Infers width and height of * The last measurement input that this state was measured with. Infers width and height of the
* the players. * players.
*/ */
var measurementInput: MeasurementInput? var measurementInput: MeasurementInput?
/** /**
* The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED]
* [EXPANDED] for fully expanded (up to 5 actions). * for fully expanded (up to 5 actions).
*/ */
var expansion: Float var expansion: Float
/** /** Fraction of the height animation. */
* Fraction of the height animation.
*/
var squishFraction: Float var squishFraction: Float
/** /** Is this host only showing active media or is it showing all of them including resumption? */
* Is this host only showing active media or is it showing all of them including resumption?
*/
var showsOnlyActiveMedia: Boolean var showsOnlyActiveMedia: Boolean
/** /** If the view should be VISIBLE or GONE. */
* If the view should be VISIBLE or GONE.
*/
val visible: Boolean val visible: Boolean
/** /** Does this host need any falsing protection? */
* Does this host need any falsing protection?
*/
var falsingProtectionNeeded: Boolean var falsingProtectionNeeded: Boolean
/** /**
* The parameters how the view disappears from this location when going to a host that's not * The parameters how the view disappears from this location when going to a host that's not
* visible. If modified, make sure to set this value again on the host to ensure the values * visible. If modified, make sure to set this value again on the host to ensure the values are
* are propagated * propagated
*/ */
var disappearParameters: DisappearParameters var disappearParameters: DisappearParameters
/** /** Get a copy of this view state, deepcopying all appropriate members */
* Get a copy of this view state, deepcopying all appropriate members
*/
fun copy(): MediaHostState fun copy(): MediaHostState
} }

View File

@@ -38,85 +38,76 @@ class MediaHostStatesManager @Inject constructor() {
*/ */
val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf() val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
/** /** A map with all media states of all locations. */
* A map with all media states of all locations.
*/
val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf() val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
/** /**
* Notify that a media state for a given location has changed. Should only be called from * Notify that a media state for a given location has changed. Should only be called from Media
* Media hosts themselves. * hosts themselves.
*/ */
fun updateHostState( fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) =
@MediaLocation location: Int, traceSection("MediaHostStatesManager#updateHostState") {
hostState: MediaHostState val currentState = mediaHostStates.get(location)
) = traceSection("MediaHostStatesManager#updateHostState") { if (!hostState.equals(currentState)) {
val currentState = mediaHostStates.get(location) val newState = hostState.copy()
if (!hostState.equals(currentState)) { mediaHostStates.put(location, newState)
val newState = hostState.copy() updateCarouselDimensions(location, hostState)
mediaHostStates.put(location, newState) // First update all the controllers to ensure they get the chance to measure
updateCarouselDimensions(location, hostState) for (controller in controllers) {
// First update all the controllers to ensure they get the chance to measure controller.stateCallback.onHostStateChanged(location, newState)
for (controller in controllers) { }
controller.stateCallback.onHostStateChanged(location, newState)
}
// Then update all other callbacks which may depend on the controllers above // Then update all other callbacks which may depend on the controllers above
for (callback in callbacks) { for (callback in callbacks) {
callback.onHostStateChanged(location, newState) callback.onHostStateChanged(location, newState)
}
} }
} }
}
/** /**
* Get the dimensions of all players combined, which determines the overall height of the * Get the dimensions of all players combined, which determines the overall height of the media
* media carousel and the media hosts. * carousel and the media hosts.
*/ */
fun updateCarouselDimensions( fun updateCarouselDimensions(
@MediaLocation location: Int, @MediaLocation location: Int,
hostState: MediaHostState hostState: MediaHostState
): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") { ): MeasurementOutput =
val result = MeasurementOutput(0, 0) traceSection("MediaHostStatesManager#updateCarouselDimensions") {
for (controller in controllers) { val result = MeasurementOutput(0, 0)
val measurement = controller.getMeasurementsForState(hostState) for (controller in controllers) {
measurement?.let { val measurement = controller.getMeasurementsForState(hostState)
if (it.measuredHeight > result.measuredHeight) { measurement?.let {
result.measuredHeight = it.measuredHeight if (it.measuredHeight > result.measuredHeight) {
} result.measuredHeight = it.measuredHeight
if (it.measuredWidth > result.measuredWidth) { }
result.measuredWidth = it.measuredWidth if (it.measuredWidth > result.measuredWidth) {
result.measuredWidth = it.measuredWidth
}
} }
} }
carouselSizes[location] = result
return result
} }
carouselSizes[location] = result
return result
}
/** /** Add a callback to be called when a MediaState has updated */
* Add a callback to be called when a MediaState has updated
*/
fun addCallback(callback: Callback) { fun addCallback(callback: Callback) {
callbacks.add(callback) callbacks.add(callback)
} }
/** /** Remove a callback that listens to media states */
* Remove a callback that listens to media states
*/
fun removeCallback(callback: Callback) { fun removeCallback(callback: Callback) {
callbacks.remove(callback) callbacks.remove(callback)
} }
/** /**
* Register a controller that listens to media states and is used to determine the size of * Register a controller that listens to media states and is used to determine the size of the
* the media carousel * media carousel
*/ */
fun addController(controller: MediaViewController) { fun addController(controller: MediaViewController) {
controllers.add(controller) controllers.add(controller)
} }
/** /** Notify the manager about the removal of a controller. */
* Notify the manager about the removal of a controller.
*/
fun removeController(controller: MediaViewController) { fun removeController(controller: MediaViewController) {
controllers.remove(controller) controllers.remove(controller)
} }

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 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.
*/
package com.android.systemui.media.controls.ui package com.android.systemui.media.controls.ui
import android.content.Context import android.content.Context
@@ -11,14 +27,13 @@ import com.android.systemui.Gefingerpoken
import com.android.wm.shell.animation.physicsAnimator import com.android.wm.shell.animation.physicsAnimator
/** /**
* A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when
* when only measuring children but not the parent, when trying to apply a new scroll position * only measuring children but not the parent, when trying to apply a new scroll position
*/ */
class MediaScrollView @JvmOverloads constructor( class MediaScrollView
context: Context, @JvmOverloads
attrs: AttributeSet? = null, constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
defStyleAttr: Int = 0 HorizontalScrollView(context, attrs, defStyleAttr) {
) : HorizontalScrollView(context, attrs, defStyleAttr) {
lateinit var contentContainer: ViewGroup lateinit var contentContainer: ViewGroup
private set private set
@@ -33,35 +48,33 @@ class MediaScrollView @JvmOverloads constructor(
* Get the current content translation. This is usually the normal translationX of the content, * Get the current content translation. This is usually the normal translationX of the content,
* but when animating, it might differ * but when animating, it might differ
*/ */
fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) { fun getContentTranslation() =
animationTargetX if (contentContainer.physicsAnimator.isRunning()) {
} else { animationTargetX
contentContainer.translationX } else {
} contentContainer.translationX
}
/** /**
* Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
* carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is
* is always absolute. This function is its own inverse. * always absolute. This function is its own inverse.
*/ */
private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) { private fun transformScrollX(scrollX: Int): Int =
contentContainer.width - width - scrollX if (isLayoutRtl) {
} else { contentContainer.width - width - scrollX
scrollX } else {
} scrollX
}
/** /** Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. */
* Get the layoutDirection-relative (start-to-end) scroll X position of the carousel.
*/
var relativeScrollX: Int var relativeScrollX: Int
get() = transformScrollX(scrollX) get() = transformScrollX(scrollX)
set(value) { set(value) {
scrollX = transformScrollX(value) scrollX = transformScrollX(value)
} }
/** /** Allow all scrolls to go through, use base implementation */
* Allow all scrolls to go through, use base implementation
*/
override fun scrollTo(x: Int, y: Int) { override fun scrollTo(x: Int, y: Int) {
if (mScrollX != x || mScrollY != y) { if (mScrollX != x || mScrollY != y) {
val oldX: Int = mScrollX val oldX: Int = mScrollX
@@ -78,17 +91,13 @@ class MediaScrollView @JvmOverloads constructor(
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false var intercept = false
touchListener?.let { touchListener?.let { intercept = it.onInterceptTouchEvent(ev) }
intercept = it.onInterceptTouchEvent(ev)
}
return super.onInterceptTouchEvent(ev) || intercept return super.onInterceptTouchEvent(ev) || intercept
} }
override fun onTouchEvent(ev: MotionEvent?): Boolean { override fun onTouchEvent(ev: MotionEvent?): Boolean {
var touch = false var touch = false
touchListener?.let { touchListener?.let { touch = it.onTouchEvent(ev) }
touch = it.onTouchEvent(ev)
}
return super.onTouchEvent(ev) || touch return super.onTouchEvent(ev) || touch
} }
@@ -112,19 +121,25 @@ class MediaScrollView @JvmOverloads constructor(
// When we're dismissing we ignore all the scrolling // When we're dismissing we ignore all the scrolling
return false return false
} }
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, return super.overScrollBy(
scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent) deltaX,
deltaY,
scrollX,
scrollY,
scrollRangeX,
scrollRangeY,
maxOverScrollX,
maxOverScrollY,
isTouchEvent
)
} }
/** /** Cancel the current touch event going on. */
* Cancel the current touch event going on.
*/
fun cancelCurrentScroll() { fun cancelCurrentScroll() {
val now = SystemClock.uptimeMillis() val now = SystemClock.uptimeMillis()
val event = MotionEvent.obtain(now, now, val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
event.source = InputDevice.SOURCE_TOUCHSCREEN event.source = InputDevice.SOURCE_TOUCHSCREEN
super.onTouchEvent(event) super.onTouchEvent(event)
event.recycle() event.recycle()
} }
} }

View File

@@ -42,7 +42,9 @@ import javax.inject.Inject
* A class responsible for controlling a single instance of a media player handling interactions * A class responsible for controlling a single instance of a media player handling interactions
* with the view instance and keeping the media view states up to date. * with the view instance and keeping the media view states up to date.
*/ */
class MediaViewController @Inject constructor( class MediaViewController
@Inject
constructor(
private val context: Context, private val context: Context,
private val configurationController: ConfigurationController, private val configurationController: ConfigurationController,
private val mediaHostStatesManager: MediaHostStatesManager, private val mediaHostStatesManager: MediaHostStatesManager,
@@ -50,17 +52,18 @@ class MediaViewController @Inject constructor(
) { ) {
/** /**
* Indicating that the media view controller is for a notification-based player, * Indicating that the media view controller is for a notification-based player, session-based
* session-based player, or recommendation * player, or recommendation
*/ */
enum class TYPE { enum class TYPE {
PLAYER, RECOMMENDATION PLAYER,
RECOMMENDATION
} }
companion object { companion object {
@JvmField @JvmField val GUTS_ANIMATION_DURATION = 500L
val GUTS_ANIMATION_DURATION = 500L val controlIds =
val controlIds = setOf( setOf(
R.id.media_progress_bar, R.id.media_progress_bar,
R.id.actionNext, R.id.actionNext,
R.id.actionPrev, R.id.actionPrev,
@@ -71,22 +74,20 @@ class MediaViewController @Inject constructor(
R.id.action4, R.id.action4,
R.id.media_scrubbing_elapsed_time, R.id.media_scrubbing_elapsed_time,
R.id.media_scrubbing_total_time R.id.media_scrubbing_total_time
) )
val detailIds = setOf( val detailIds =
setOf(
R.id.header_title, R.id.header_title,
R.id.header_artist, R.id.header_artist,
R.id.actionPlayPause, R.id.actionPlayPause,
) )
} }
/** /** A listener when the current dimensions of the player change */
* A listener when the current dimensions of the player change
*/
lateinit var sizeChangedListener: () -> Unit lateinit var sizeChangedListener: () -> Unit
private var firstRefresh: Boolean = true private var firstRefresh: Boolean = true
@VisibleForTesting @VisibleForTesting private var transitionLayout: TransitionLayout? = null
private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController() private val layoutController = TransitionLayoutController()
private var animationDelay: Long = 0 private var animationDelay: Long = 0
private var animationDuration: Long = 0 private var animationDuration: Long = 0
@@ -94,116 +95,98 @@ class MediaViewController @Inject constructor(
private val measurement = MeasurementOutput(0, 0) private val measurement = MeasurementOutput(0, 0)
private var type: TYPE = TYPE.PLAYER private var type: TYPE = TYPE.PLAYER
/** /** A map containing all viewStates for all locations of this mediaState */
* A map containing all viewStates for all locations of this mediaState
*/
private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
/** /**
* The ending location of the view where it ends when all animations and transitions have * The ending location of the view where it ends when all animations and transitions have
* finished * finished
*/ */
@MediaLocation @MediaLocation var currentEndLocation: Int = -1
var currentEndLocation: Int = -1
/** /** The starting location of the view where it starts for all animations and transitions */
* The starting location of the view where it starts for all animations and transitions @MediaLocation private var currentStartLocation: Int = -1
*/
@MediaLocation
private var currentStartLocation: Int = -1
/** /** The progress of the transition or 1.0 if there is no transition happening */
* The progress of the transition or 1.0 if there is no transition happening
*/
private var currentTransitionProgress: Float = 1.0f private var currentTransitionProgress: Float = 1.0f
/** /** A temporary state used to store intermediate measurements. */
* A temporary state used to store intermediate measurements.
*/
private val tmpState = TransitionViewState() private val tmpState = TransitionViewState()
/** /** A temporary state used to store intermediate measurements. */
* A temporary state used to store intermediate measurements.
*/
private val tmpState2 = TransitionViewState() private val tmpState2 = TransitionViewState()
/** /** A temporary state used to store intermediate measurements. */
* A temporary state used to store intermediate measurements.
*/
private val tmpState3 = TransitionViewState() private val tmpState3 = TransitionViewState()
/** /** A temporary cache key to be used to look up cache entries */
* A temporary cache key to be used to look up cache entries
*/
private val tmpKey = CacheKey() private val tmpKey = CacheKey()
/** /**
* The current width of the player. This might not factor in case the player is animating * The current width of the player. This might not factor in case the player is animating to the
* to the current state, but represents the end state * current state, but represents the end state
*/ */
var currentWidth: Int = 0 var currentWidth: Int = 0
/** /**
* The current height of the player. This might not factor in case the player is animating * The current height of the player. This might not factor in case the player is animating to
* to the current state, but represents the end state * the current state, but represents the end state
*/ */
var currentHeight: Int = 0 var currentHeight: Int = 0
/** /** Get the translationX of the layout */
* Get the translationX of the layout
*/
var translationX: Float = 0.0f var translationX: Float = 0.0f
private set private set
get() { get() {
return transitionLayout?.translationX ?: 0.0f return transitionLayout?.translationX ?: 0.0f
} }
/** /** Get the translationY of the layout */
* Get the translationY of the layout
*/
var translationY: Float = 0.0f var translationY: Float = 0.0f
private set private set
get() { get() {
return transitionLayout?.translationY ?: 0.0f return transitionLayout?.translationY ?: 0.0f
} }
/** /** A callback for RTL config changes */
* A callback for RTL config changes private val configurationListener =
*/ object : ConfigurationController.ConfigurationListener {
private val configurationListener = object : ConfigurationController.ConfigurationListener { override fun onConfigChanged(newConfig: Configuration?) {
override fun onConfigChanged(newConfig: Configuration?) { // Because the TransitionLayout is not always attached (and calculates/caches layout
// Because the TransitionLayout is not always attached (and calculates/caches layout // results regardless of attach state), we have to force the layoutDirection of the
// results regardless of attach state), we have to force the layoutDirection of the view // view
// to the correct value for the user's current locale to ensure correct recalculation // to the correct value for the user's current locale to ensure correct
// when/after calling refreshState() // recalculation
newConfig?.apply { // when/after calling refreshState()
if (transitionLayout?.rawLayoutDirection != layoutDirection) { newConfig?.apply {
transitionLayout?.layoutDirection = layoutDirection if (transitionLayout?.rawLayoutDirection != layoutDirection) {
refreshState() transitionLayout?.layoutDirection = layoutDirection
refreshState()
}
} }
} }
} }
}
/** /** A callback for media state changes */
* A callback for media state changes val stateCallback =
*/ object : MediaHostStatesManager.Callback {
val stateCallback = object : MediaHostStatesManager.Callback { override fun onHostStateChanged(
override fun onHostStateChanged( @MediaLocation location: Int,
@MediaLocation location: Int, mediaHostState: MediaHostState
mediaHostState: MediaHostState ) {
) { if (location == currentEndLocation || location == currentStartLocation) {
if (location == currentEndLocation || location == currentStartLocation) { setCurrentState(
setCurrentState(currentStartLocation, currentStartLocation,
currentEndLocation, currentEndLocation,
currentTransitionProgress, currentTransitionProgress,
applyImmediately = false) applyImmediately = false
)
}
} }
} }
}
/** /**
* The expanded constraint set used to render a expanded player. If it is modified, make sure * The expanded constraint set used to render a expanded player. If it is modified, make sure to
* to call [refreshState] * call [refreshState]
*/ */
val collapsedLayout = ConstraintSet() val collapsedLayout = ConstraintSet()
@@ -213,9 +196,7 @@ class MediaViewController @Inject constructor(
*/ */
val expandedLayout = ConstraintSet() val expandedLayout = ConstraintSet()
/** /** Whether the guts are visible for the associated player. */
* Whether the guts are visible for the associated player.
*/
var isGutsVisible = false var isGutsVisible = false
private set private set
@@ -237,17 +218,17 @@ class MediaViewController @Inject constructor(
configurationController.removeCallback(configurationListener) configurationController.removeCallback(configurationListener)
} }
/** /** Show guts with an animated transition. */
* Show guts with an animated transition.
*/
fun openGuts() { fun openGuts() {
if (isGutsVisible) return if (isGutsVisible) return
isGutsVisible = true isGutsVisible = true
animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
setCurrentState(currentStartLocation, setCurrentState(
currentEndLocation, currentStartLocation,
currentTransitionProgress, currentEndLocation,
applyImmediately = false) currentTransitionProgress,
applyImmediately = false
)
} }
/** /**
@@ -262,10 +243,12 @@ class MediaViewController @Inject constructor(
if (!immediate) { if (!immediate) {
animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
} }
setCurrentState(currentStartLocation, setCurrentState(
currentEndLocation, currentStartLocation,
currentTransitionProgress, currentEndLocation,
applyImmediately = immediate) currentTransitionProgress,
applyImmediately = immediate
)
} }
private fun ensureAllMeasurements() { private fun ensureAllMeasurements() {
@@ -275,21 +258,20 @@ class MediaViewController @Inject constructor(
} }
} }
/** /** Get the constraintSet for a given expansion */
* Get the constraintSet for a given expansion
*/
private fun constraintSetForExpansion(expansion: Float): ConstraintSet = private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
if (expansion > 0) expandedLayout else collapsedLayout if (expansion > 0) expandedLayout else collapsedLayout
/** /**
* Set the views to be showing/hidden based on the [isGutsVisible] for a given * Set the views to be showing/hidden based on the [isGutsVisible] for a given
* [TransitionViewState]. * [TransitionViewState].
*/ */
private fun setGutsViewState(viewState: TransitionViewState) { private fun setGutsViewState(viewState: TransitionViewState) {
val controlsIds = when (type) { val controlsIds =
TYPE.PLAYER -> MediaViewHolder.controlsIds when (type) {
TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds TYPE.PLAYER -> MediaViewHolder.controlsIds
} TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
}
val gutsIds = GutsViewHolder.ids val gutsIds = GutsViewHolder.ids
controlsIds.forEach { id -> controlsIds.forEach { id ->
viewState.widgetStates.get(id)?.let { state -> viewState.widgetStates.get(id)?.let { state ->
@@ -307,9 +289,7 @@ class MediaViewController @Inject constructor(
} }
} }
/** /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */
* Apply squishFraction to a copy of viewState such that the cached version is untouched.
*/
internal fun squishViewState( internal fun squishViewState(
viewState: TransitionViewState, viewState: TransitionViewState,
squishFraction: Float squishFraction: Float
@@ -347,8 +327,8 @@ class MediaViewController @Inject constructor(
* Obtain a new viewState for a given media state. This usually returns a cached state, but if * Obtain a new viewState for a given media state. This usually returns a cached state, but if
* it's not available, it will recreate one by measuring, which may be expensive. * it's not available, it will recreate one by measuring, which may be expensive.
*/ */
@VisibleForTesting @VisibleForTesting
fun obtainViewState(state: MediaHostState?): TransitionViewState? { fun obtainViewState(state: MediaHostState?): TransitionViewState? {
if (state == null || state.measurementInput == null) { if (state == null || state.measurementInput == null) {
return null return null
} }
@@ -371,10 +351,12 @@ class MediaViewController @Inject constructor(
} }
// Let's create a new measurement // Let's create a new measurement
if (state.expansion == 0.0f || state.expansion == 1.0f) { if (state.expansion == 0.0f || state.expansion == 1.0f) {
result = transitionLayout!!.calculateViewState( result =
transitionLayout!!.calculateViewState(
state.measurementInput!!, state.measurementInput!!,
constraintSetForExpansion(state.expansion), constraintSetForExpansion(state.expansion),
TransitionViewState()) TransitionViewState()
)
setGutsViewState(result) setGutsViewState(result)
// We don't want to cache interpolated or null states as this could quickly fill up // We don't want to cache interpolated or null states as this could quickly fill up
@@ -390,10 +372,8 @@ class MediaViewController @Inject constructor(
val startViewState = obtainViewState(startState) as TransitionViewState val startViewState = obtainViewState(startState) as TransitionViewState
val endState = state.copy().also { it.expansion = 1.0f } val endState = state.copy().also { it.expansion = 1.0f }
val endViewState = obtainViewState(endState) as TransitionViewState val endViewState = obtainViewState(endState) as TransitionViewState
result = layoutController.getInterpolatedState( result =
startViewState, layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
endViewState,
state.expansion)
} }
if (state.squishFraction <= 1f) { if (state.squishFraction <= 1f) {
return squishViewState(result, state.squishFraction) return squishViewState(result, state.squishFraction)
@@ -401,11 +381,7 @@ class MediaViewController @Inject constructor(
return result return result
} }
private fun getKey( private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
state: MediaHostState,
guts: Boolean,
result: CacheKey
): CacheKey {
result.apply { result.apply {
heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
@@ -416,41 +392,39 @@ class MediaViewController @Inject constructor(
} }
/** /**
* Attach a view to this controller. This may perform measurements if it's not available yet * Attach a view to this controller. This may perform measurements if it's not available yet and
* and should therefore be done carefully. * should therefore be done carefully.
*/ */
fun attach( fun attach(transitionLayout: TransitionLayout, type: TYPE) =
transitionLayout: TransitionLayout, traceSection("MediaViewController#attach") {
type: TYPE updateMediaViewControllerType(type)
) = traceSection("MediaViewController#attach") { logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
updateMediaViewControllerType(type) this.transitionLayout = transitionLayout
logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) layoutController.attach(transitionLayout)
this.transitionLayout = transitionLayout if (currentEndLocation == -1) {
layoutController.attach(transitionLayout) return
if (currentEndLocation == -1) { }
return // Set the previously set state immediately to the view, now that it's finally attached
} setCurrentState(
// Set the previously set state immediately to the view, now that it's finally attached
setCurrentState(
startLocation = currentStartLocation, startLocation = currentStartLocation,
endLocation = currentEndLocation, endLocation = currentEndLocation,
transitionProgress = currentTransitionProgress, transitionProgress = currentTransitionProgress,
applyImmediately = true) applyImmediately = true
} )
}
/** /**
* Obtain a measurement for a given location. This makes sure that the state is up to date * Obtain a measurement for a given location. This makes sure that the state is up to date and
* and all widgets know their location. Calling this method may create a measurement if we * all widgets know their location. Calling this method may create a measurement if we don't
* don't have a cached value available already. * have a cached value available already.
*/ */
fun getMeasurementsForState( fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? =
hostState: MediaHostState traceSection("MediaViewController#getMeasurementsForState") {
): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") { val viewState = obtainViewState(hostState) ?: return null
val viewState = obtainViewState(hostState) ?: return null measurement.measuredWidth = viewState.width
measurement.measuredWidth = viewState.width measurement.measuredHeight = viewState.height
measurement.measuredHeight = viewState.height return measurement
return measurement }
}
/** /**
* Set a new state for the controlled view which can be an interpolation between multiple * Set a new state for the controlled view which can be an interpolation between multiple
@@ -461,67 +435,85 @@ class MediaViewController @Inject constructor(
@MediaLocation endLocation: Int, @MediaLocation endLocation: Int,
transitionProgress: Float, transitionProgress: Float,
applyImmediately: Boolean applyImmediately: Boolean
) = traceSection("MediaViewController#setCurrentState") { ) =
currentEndLocation = endLocation traceSection("MediaViewController#setCurrentState") {
currentStartLocation = startLocation currentEndLocation = endLocation
currentTransitionProgress = transitionProgress currentStartLocation = startLocation
logger.logMediaLocation("setCurrentState", startLocation, endLocation) currentTransitionProgress = transitionProgress
logger.logMediaLocation("setCurrentState", startLocation, endLocation)
val shouldAnimate = animateNextStateChange && !applyImmediately val shouldAnimate = animateNextStateChange && !applyImmediately
val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
// Obtain the view state that we'd want to be at the end // Obtain the view state that we'd want to be at the end
// The view might not be bound yet or has never been measured and in that case will be // The view might not be bound yet or has never been measured and in that case will be
// reset once the state is fully available // reset once the state is fully available
var endViewState = obtainViewState(endHostState) ?: return var endViewState = obtainViewState(endHostState) ?: return
endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!! endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
layoutController.setMeasureState(endViewState) layoutController.setMeasureState(endViewState)
// If the view isn't bound, we can drop the animation, otherwise we'll execute it // If the view isn't bound, we can drop the animation, otherwise we'll execute it
animateNextStateChange = false animateNextStateChange = false
if (transitionLayout == null) { if (transitionLayout == null) {
return return
}
val result: TransitionViewState
var startViewState = obtainViewState(startHostState)
startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
if (!endHostState.visible) {
// Let's handle the case where the end is gone first. In this case we take the
// start viewState and will make it gone
if (startViewState == null || startHostState == null || !startHostState.visible) {
// the start isn't a valid state, let's use the endstate directly
result = endViewState
} else {
// Let's get the gone presentation from the start state
result = layoutController.getGoneState(startViewState,
startHostState.disappearParameters,
transitionProgress,
tmpState)
} }
} else if (startHostState != null && !startHostState.visible) {
// We have a start state and it is gone. val result: TransitionViewState
// Let's get presentation from the endState var startViewState = obtainViewState(startHostState)
result = layoutController.getGoneState(endViewState, endHostState.disappearParameters, startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
1.0f - transitionProgress,
tmpState) if (!endHostState.visible) {
} else if (transitionProgress == 1.0f || startViewState == null) { // Let's handle the case where the end is gone first. In this case we take the
// We're at the end. Let's use that state // start viewState and will make it gone
result = endViewState if (startViewState == null || startHostState == null || !startHostState.visible) {
} else if (transitionProgress == 0.0f) { // the start isn't a valid state, let's use the endstate directly
// We're at the start. Let's use that state result = endViewState
result = startViewState } else {
} else { // Let's get the gone presentation from the start state
result = layoutController.getInterpolatedState(startViewState, endViewState, result =
transitionProgress, tmpState) layoutController.getGoneState(
startViewState,
startHostState.disappearParameters,
transitionProgress,
tmpState
)
}
} else if (startHostState != null && !startHostState.visible) {
// We have a start state and it is gone.
// Let's get presentation from the endState
result =
layoutController.getGoneState(
endViewState,
endHostState.disappearParameters,
1.0f - transitionProgress,
tmpState
)
} else if (transitionProgress == 1.0f || startViewState == null) {
// We're at the end. Let's use that state
result = endViewState
} else if (transitionProgress == 0.0f) {
// We're at the start. Let's use that state
result = startViewState
} else {
result =
layoutController.getInterpolatedState(
startViewState,
endViewState,
transitionProgress,
tmpState
)
}
logger.logMediaSize("setCurrentState", result.width, result.height)
layoutController.setState(
result,
applyImmediately,
shouldAnimate,
animationDuration,
animationDelay
)
} }
logger.logMediaSize("setCurrentState", result.width, result.height)
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
animationDelay)
}
private fun updateViewStateToCarouselSize( private fun updateViewStateToCarouselSize(
viewState: TransitionViewState?, viewState: TransitionViewState?,
@@ -558,8 +550,8 @@ class MediaViewController @Inject constructor(
} }
/** /**
* Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event
* In the event of [location] not being visible, [locationWhenHidden] will be used instead. * of [location] not being visible, [locationWhenHidden] will be used instead.
* *
* @param location Target * @param location Target
* @param locationWhenHidden Location that will be used when the target is not * @param locationWhenHidden Location that will be used when the target is not
@@ -576,40 +568,37 @@ class MediaViewController @Inject constructor(
* This updates the width the view will me measured with. * This updates the width the view will me measured with.
*/ */
fun onLocationPreChange(@MediaLocation newLocation: Int) { fun onLocationPreChange(@MediaLocation newLocation: Int) {
obtainViewStateForLocation(newLocation)?.let { obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) }
layoutController.setMeasureState(it)
}
} }
/** /** Request that the next state change should be animated with the given parameters. */
* Request that the next state change should be animated with the given parameters.
*/
fun animatePendingStateChange(duration: Long, delay: Long) { fun animatePendingStateChange(duration: Long, delay: Long) {
animateNextStateChange = true animateNextStateChange = true
animationDuration = duration animationDuration = duration
animationDelay = delay animationDelay = delay
} }
/** /** Clear all existing measurements and refresh the state to match the view. */
* Clear all existing measurements and refresh the state to match the view. fun refreshState() =
*/ traceSection("MediaViewController#refreshState") {
fun refreshState() = traceSection("MediaViewController#refreshState") { // Let's clear all of our measurements and recreate them!
// Let's clear all of our measurements and recreate them! viewStates.clear()
viewStates.clear() if (firstRefresh) {
if (firstRefresh) { // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
// This is the first bind, let's ensure we pre-cache all measurements. Otherwise // We'll just load these on demand.
// We'll just load these on demand. ensureAllMeasurements()
ensureAllMeasurements() firstRefresh = false
firstRefresh = false }
setCurrentState(
currentStartLocation,
currentEndLocation,
currentTransitionProgress,
applyImmediately = true
)
} }
setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
applyImmediately = true)
}
} }
/** /** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */
* An internal key for the cache of mediaViewStates. This is a subset of the full host state.
*/
private data class CacheKey( private data class CacheKey(
var widthMeasureSpec: Int = -1, var widthMeasureSpec: Int = -1,
var heightMeasureSpec: Int = -1, var heightMeasureSpec: Int = -1,

View File

@@ -24,40 +24,32 @@ import javax.inject.Inject
private const val TAG = "MediaView" private const val TAG = "MediaView"
/** /** A buffered log for media view events that are too noisy for regular logging */
* A buffered log for media view events that are too noisy for regular logging
*/
@SysUISingleton @SysUISingleton
class MediaViewLogger @Inject constructor( class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogBuffer) {
@MediaViewLog private val buffer: LogBuffer
) {
fun logMediaSize(reason: String, width: Int, height: Int) { fun logMediaSize(reason: String, width: Int, height: Int) {
buffer.log( buffer.log(
TAG, TAG,
LogLevel.DEBUG, LogLevel.DEBUG,
{ {
str1 = reason str1 = reason
int1 = width int1 = width
int2 = height int2 = height
}, },
{ { "size ($str1): $int1 x $int2" }
"size ($str1): $int1 x $int2"
}
) )
} }
fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) { fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
buffer.log( buffer.log(
TAG, TAG,
LogLevel.DEBUG, LogLevel.DEBUG,
{ {
str1 = reason str1 = reason
int1 = startLocation int1 = startLocation
int2 = endLocation int2 = endLocation
}, },
{ { "location ($str1): $int1 -> $int2" }
"location ($str1): $int1 -> $int2"
}
) )
} }
} }

View File

@@ -73,4 +73,4 @@ internal open class MetadataAnimationHandler(
exitAnimator.addListener(this) exitAnimator.addListener(this)
enterAnimator.addListener(this) enterAnimator.addListener(this)
} }
} }

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 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.
*/
package com.android.systemui.media.controls.ui package com.android.systemui.media.controls.ui
import android.animation.Animator import android.animation.Animator
@@ -23,8 +39,7 @@ import kotlin.math.cos
private const val TAG = "Squiggly" private const val TAG = "Squiggly"
private const val TWO_PI = (Math.PI * 2f).toFloat() private const val TWO_PI = (Math.PI * 2f).toFloat()
@VisibleForTesting @VisibleForTesting internal const val DISABLED_ALPHA = 77
internal const val DISABLED_ALPHA = 77
class SquigglyProgress : Drawable() { class SquigglyProgress : Drawable() {
@@ -86,26 +101,29 @@ class SquigglyProgress : Drawable() {
lastFrameTime = SystemClock.uptimeMillis() lastFrameTime = SystemClock.uptimeMillis()
} }
heightAnimator?.cancel() heightAnimator?.cancel()
heightAnimator = ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply { heightAnimator =
if (animate) { ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
startDelay = 60 if (animate) {
duration = 800 startDelay = 60
interpolator = Interpolators.EMPHASIZED_DECELERATE duration = 800
} else { interpolator = Interpolators.EMPHASIZED_DECELERATE
duration = 550 } else {
interpolator = Interpolators.STANDARD_DECELERATE duration = 550
} interpolator = Interpolators.STANDARD_DECELERATE
addUpdateListener {
heightFraction = it.animatedValue as Float
invalidateSelf()
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
heightAnimator = null
} }
}) addUpdateListener {
start() heightFraction = it.animatedValue as Float
} invalidateSelf()
}
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
heightAnimator = null
}
}
)
start()
}
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
@@ -120,9 +138,15 @@ class SquigglyProgress : Drawable() {
val progress = level / 10_000f val progress = level / 10_000f
val totalWidth = bounds.width().toFloat() val totalWidth = bounds.width().toFloat()
val totalProgressPx = totalWidth * progress val totalProgressPx = totalWidth * progress
val waveProgressPx = totalWidth * ( val waveProgressPx =
if (!transitionEnabled || progress > matchedWaveEndpoint) progress else totalWidth *
lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress))) (if (!transitionEnabled || progress > matchedWaveEndpoint) progress
else
lerp(
minWaveEndpoint,
matchedWaveEndpoint,
lerpInv(0f, matchedWaveEndpoint, progress)
))
// Build Wiggly Path // Build Wiggly Path
val waveStart = -phaseOffset - waveLength / 2f val waveStart = -phaseOffset - waveLength / 2f
@@ -132,10 +156,8 @@ class SquigglyProgress : Drawable() {
val computeAmplitude: (Float, Float) -> Float = { x, sign -> val computeAmplitude: (Float, Float) -> Float = { x, sign ->
if (transitionEnabled) { if (transitionEnabled) {
val length = transitionPeriods * waveLength val length = transitionPeriods * waveLength
val coeff = lerpInvSat( val coeff =
waveProgressPx + length / 2f, lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x)
waveProgressPx - length / 2f,
x)
sign * heightFraction * lineAmplitude * coeff sign * heightFraction * lineAmplitude * coeff
} else { } else {
sign * heightFraction * lineAmplitude sign * heightFraction * lineAmplitude
@@ -156,10 +178,7 @@ class SquigglyProgress : Drawable() {
val nextX = currentX + dist val nextX = currentX + dist
val midX = currentX + dist / 2 val midX = currentX + dist / 2
val nextAmp = computeAmplitude(nextX, waveSign) val nextAmp = computeAmplitude(nextX, waveSign)
path.cubicTo( path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp)
midX, currentAmp,
midX, nextAmp,
nextX, nextAmp)
currentAmp = nextAmp currentAmp = nextAmp
currentX = nextX currentX = nextX
} }
@@ -229,7 +248,7 @@ class SquigglyProgress : Drawable() {
private fun updateColors(tintColor: Int, alpha: Int) { private fun updateColors(tintColor: Int, alpha: Int) {
wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha) wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha)
linePaint.color = ColorUtils.setAlphaComponent(tintColor, linePaint.color =
(DISABLED_ALPHA * (alpha / 255f)).toInt()) ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt())
} }
} }

View File

@@ -20,9 +20,7 @@ import android.content.Context
import com.android.systemui.util.Utils import com.android.systemui.util.Utils
import javax.inject.Inject import javax.inject.Inject
/** /** Provides access to the current value of the feature flag. */
* Provides access to the current value of the feature flag.
*/
class MediaFeatureFlag @Inject constructor(private val context: Context) { class MediaFeatureFlag @Inject constructor(private val context: Context) {
val enabled val enabled
get() = Utils.useQsMediaPlayer(context) get() = Utils.useQsMediaPlayer(context)

View File

@@ -34,9 +34,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS) return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
} }
/** /** Check whether we support displaying information about mute await connections. */
* Check whether we support displaying information about mute await connections.
*/
fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT) fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT)
/** /**

View File

@@ -30,17 +30,13 @@ import javax.inject.Inject
private const val INSTANCE_ID_MAX = 1 shl 20 private const val INSTANCE_ID_MAX = 1 shl 20
/** /** A helper class to log events related to the media controls */
* A helper class to log events related to the media controls
*/
@SysUISingleton @SysUISingleton
class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) { class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) {
private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX) private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX)
/** /** Get a new instance ID for a new media control */
* Get a new instance ID for a new media control
*/
fun getNewInstanceId(): InstanceId { fun getNewInstanceId(): InstanceId {
return instanceIdSequence.newInstanceId() return instanceIdSequence.newInstanceId()
} }
@@ -51,12 +47,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
instanceId: InstanceId, instanceId: InstanceId,
playbackLocation: Int playbackLocation: Int
) { ) {
val event = when (playbackLocation) { val event =
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED when (playbackLocation) {
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
else -> throw IllegalArgumentException("Unknown playback location") MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
} else -> throw IllegalArgumentException("Unknown playback location")
}
logger.logWithInstanceId(event, uid, packageName, instanceId) logger.logWithInstanceId(event, uid, packageName, instanceId)
} }
@@ -66,12 +63,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
instanceId: InstanceId, instanceId: InstanceId,
playbackLocation: Int playbackLocation: Int
) { ) {
val event = when (playbackLocation) { val event =
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL when (playbackLocation) {
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
else -> throw IllegalArgumentException("Unknown playback location") MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
} else -> throw IllegalArgumentException("Unknown playback location")
}
logger.logWithInstanceId(event, uid, packageName, instanceId) logger.logWithInstanceId(event, uid, packageName, instanceId)
} }
@@ -110,8 +108,12 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
} }
fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) { fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName, logger.logWithInstanceId(
instanceId) MediaUiEvent.OPEN_SETTINGS_LONG_PRESS,
uid,
packageName,
instanceId
)
} }
fun logCarouselSettings() { fun logCarouselSettings() {
@@ -120,12 +122,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
} }
fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) { fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) {
val event = when (buttonId) { val event =
R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE when (buttonId) {
R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
else -> MediaUiEvent.TAP_ACTION_OTHER R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
} else -> MediaUiEvent.TAP_ACTION_OTHER
}
logger.logWithInstanceId(event, uid, packageName, instanceId) logger.logWithInstanceId(event, uid, packageName, instanceId)
} }
@@ -143,148 +146,130 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
} }
fun logCarouselPosition(@MediaLocation location: Int) { fun logCarouselPosition(@MediaLocation location: Int) {
val event = when (location) { val event =
MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS when (location) {
MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
MediaHierarchyManager.LOCATION_LOCKSCREEN -> MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN MediaHierarchyManager.LOCATION_LOCKSCREEN ->
MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
else -> throw IllegalArgumentException("Unknown media carousel location $location") MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
} else -> throw IllegalArgumentException("Unknown media carousel location $location")
}
logger.log(event) logger.log(event)
} }
fun logRecommendationAdded(packageName: String, instanceId: InstanceId) { fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName, logger.logWithInstanceId(
instanceId) MediaUiEvent.MEDIA_RECOMMENDATION_ADDED,
0,
packageName,
instanceId
)
} }
fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) { fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName, logger.logWithInstanceId(
instanceId) MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED,
0,
packageName,
instanceId
)
} }
fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) { fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName, logger.logWithInstanceId(
instanceId) MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED,
uid,
packageName,
instanceId
)
} }
fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) { fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0, logger.logWithInstanceIdAndPosition(
packageName, instanceId, position) MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP,
0,
packageName,
instanceId,
position
)
} }
fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) { fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName, logger.logWithInstanceId(
instanceId) MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP,
0,
packageName,
instanceId
)
} }
fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) { fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName, logger.logWithInstanceId(
instanceId) MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG,
uid,
packageName,
instanceId
)
} }
} }
enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "A new media control was added for media playing locally on the device") @UiEvent(doc = "A new media control was added for media playing locally on the device")
LOCAL_MEDIA_ADDED(1029), LOCAL_MEDIA_ADDED(1029),
@UiEvent(doc = "A new media control was added for media cast from the device") @UiEvent(doc = "A new media control was added for media cast from the device")
CAST_MEDIA_ADDED(1030), CAST_MEDIA_ADDED(1030),
@UiEvent(doc = "A new media control was added for media playing remotely") @UiEvent(doc = "A new media control was added for media playing remotely")
REMOTE_MEDIA_ADDED(1031), REMOTE_MEDIA_ADDED(1031),
@UiEvent(doc = "The media for an existing control was transferred to local playback") @UiEvent(doc = "The media for an existing control was transferred to local playback")
TRANSFER_TO_LOCAL(1032), TRANSFER_TO_LOCAL(1032),
@UiEvent(doc = "The media for an existing control was transferred to a cast device") @UiEvent(doc = "The media for an existing control was transferred to a cast device")
TRANSFER_TO_CAST(1033), TRANSFER_TO_CAST(1033),
@UiEvent(doc = "The media for an existing control was transferred to a remote device") @UiEvent(doc = "The media for an existing control was transferred to a remote device")
TRANSFER_TO_REMOTE(1034), TRANSFER_TO_REMOTE(1034),
@UiEvent(doc = "A new resumable media control was added") RESUME_MEDIA_ADDED(1013),
@UiEvent(doc = "A new resumable media control was added")
RESUME_MEDIA_ADDED(1013),
@UiEvent(doc = "An existing active media control was converted into resumable media") @UiEvent(doc = "An existing active media control was converted into resumable media")
ACTIVE_TO_RESUME(1014), ACTIVE_TO_RESUME(1014),
@UiEvent(doc = "A media control timed out") MEDIA_TIMEOUT(1015),
@UiEvent(doc = "A media control timed out") @UiEvent(doc = "A media control was removed from the carousel") MEDIA_REMOVED(1016),
MEDIA_TIMEOUT(1015), @UiEvent(doc = "User swiped to another control within the media carousel") CAROUSEL_PAGE(1017),
@UiEvent(doc = "The user swiped away the media carousel") DISMISS_SWIPE(1018),
@UiEvent(doc = "A media control was removed from the carousel") @UiEvent(doc = "The user long pressed on a media control") OPEN_LONG_PRESS(1019),
MEDIA_REMOVED(1016),
@UiEvent(doc = "User swiped to another control within the media carousel")
CAROUSEL_PAGE(1017),
@UiEvent(doc = "The user swiped away the media carousel")
DISMISS_SWIPE(1018),
@UiEvent(doc = "The user long pressed on a media control")
OPEN_LONG_PRESS(1019),
@UiEvent(doc = "The user dismissed a media control via its long press menu") @UiEvent(doc = "The user dismissed a media control via its long press menu")
DISMISS_LONG_PRESS(1020), DISMISS_LONG_PRESS(1020),
@UiEvent(doc = "The user opened media settings from a media control's long press menu") @UiEvent(doc = "The user opened media settings from a media control's long press menu")
OPEN_SETTINGS_LONG_PRESS(1021), OPEN_SETTINGS_LONG_PRESS(1021),
@UiEvent(doc = "The user opened media settings from the media carousel") @UiEvent(doc = "The user opened media settings from the media carousel")
OPEN_SETTINGS_CAROUSEL(1022), OPEN_SETTINGS_CAROUSEL(1022),
@UiEvent(doc = "The play/pause button on a media control was tapped") @UiEvent(doc = "The play/pause button on a media control was tapped")
TAP_ACTION_PLAY_PAUSE(1023), TAP_ACTION_PLAY_PAUSE(1023),
@UiEvent(doc = "The previous button on a media control was tapped") TAP_ACTION_PREV(1024),
@UiEvent(doc = "The previous button on a media control was tapped") @UiEvent(doc = "The next button on a media control was tapped") TAP_ACTION_NEXT(1025),
TAP_ACTION_PREV(1024),
@UiEvent(doc = "The next button on a media control was tapped")
TAP_ACTION_NEXT(1025),
@UiEvent(doc = "A custom or generic action button on a media control was tapped") @UiEvent(doc = "A custom or generic action button on a media control was tapped")
TAP_ACTION_OTHER(1026), TAP_ACTION_OTHER(1026),
@UiEvent(doc = "The user seeked on a media control using the seekbar") ACTION_SEEK(1027),
@UiEvent(doc = "The user seeked on a media control using the seekbar")
ACTION_SEEK(1027),
@UiEvent(doc = "The user opened the output switcher from a media control") @UiEvent(doc = "The user opened the output switcher from a media control")
OPEN_OUTPUT_SWITCHER(1028), OPEN_OUTPUT_SWITCHER(1028),
@UiEvent(doc = "The user tapped on a media control view") MEDIA_TAP_CONTENT_VIEW(1036),
@UiEvent(doc = "The user tapped on a media control view") @UiEvent(doc = "The media carousel moved to QQS") MEDIA_CAROUSEL_LOCATION_QQS(1037),
MEDIA_TAP_CONTENT_VIEW(1036), @UiEvent(doc = "THe media carousel moved to QS") MEDIA_CAROUSEL_LOCATION_QS(1038),
@UiEvent(doc = "The media carousel moved to QQS")
MEDIA_CAROUSEL_LOCATION_QQS(1037),
@UiEvent(doc = "THe media carousel moved to QS")
MEDIA_CAROUSEL_LOCATION_QS(1038),
@UiEvent(doc = "The media carousel moved to the lockscreen") @UiEvent(doc = "The media carousel moved to the lockscreen")
MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039), MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
@UiEvent(doc = "The media carousel moved to the dream state") @UiEvent(doc = "The media carousel moved to the dream state")
MEDIA_CAROUSEL_LOCATION_DREAM(1040), MEDIA_CAROUSEL_LOCATION_DREAM(1040),
@UiEvent(doc = "A media recommendation card was added to the media carousel") @UiEvent(doc = "A media recommendation card was added to the media carousel")
MEDIA_RECOMMENDATION_ADDED(1041), MEDIA_RECOMMENDATION_ADDED(1041),
@UiEvent(doc = "A media recommendation card was removed from the media carousel") @UiEvent(doc = "A media recommendation card was removed from the media carousel")
MEDIA_RECOMMENDATION_REMOVED(1042), MEDIA_RECOMMENDATION_REMOVED(1042),
@UiEvent(doc = "An existing media control was made active as a recommendation") @UiEvent(doc = "An existing media control was made active as a recommendation")
MEDIA_RECOMMENDATION_ACTIVATED(1043), MEDIA_RECOMMENDATION_ACTIVATED(1043),
@UiEvent(doc = "User tapped on an item in a media recommendation card") @UiEvent(doc = "User tapped on an item in a media recommendation card")
MEDIA_RECOMMENDATION_ITEM_TAP(1044), MEDIA_RECOMMENDATION_ITEM_TAP(1044),
@UiEvent(doc = "User tapped on a media recommendation card") @UiEvent(doc = "User tapped on a media recommendation card")
MEDIA_RECOMMENDATION_CARD_TAP(1045), MEDIA_RECOMMENDATION_CARD_TAP(1045),
@UiEvent(doc = "User opened the broadcast dialog from a media control") @UiEvent(doc = "User opened the broadcast dialog from a media control")
MEDIA_OPEN_BROADCAST_DIALOG(1079); MEDIA_OPEN_BROADCAST_DIALOG(1079);
override fun getId() = metricId override fun getId() = metricId
} }

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 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.
*/
package com.android.systemui.media.controls package com.android.systemui.media.controls
import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceId
@@ -5,24 +21,26 @@ import com.android.systemui.media.controls.models.player.MediaData
class MediaTestUtils { class MediaTestUtils {
companion object { companion object {
val emptyMediaData = MediaData( val emptyMediaData =
userId = 0, MediaData(
initialized = true, userId = 0,
app = null, initialized = true,
appIcon = null, app = null,
artist = null, appIcon = null,
song = null, artist = null,
artwork = null, song = null,
actions = emptyList(), artwork = null,
actionsToShowInCompact = emptyList(), actions = emptyList(),
packageName = "", actionsToShowInCompact = emptyList(),
token = null, packageName = "",
clickIntent = null, token = null,
device = null, clickIntent = null,
active = true, device = null,
resumeAction = null, active = true,
isPlaying = false, resumeAction = null,
instanceId = InstanceId.fakeInstanceId(-1), isPlaying = false,
appUid = -1) instanceId = InstanceId.fakeInstanceId(-1),
appUid = -1
)
} }
} }

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 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.
*/
package com.android.systemui.media.controls.models.player package com.android.systemui.media.controls.models.player
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
@@ -21,4 +37,4 @@ class MediaViewHolderTest : SysuiTestCase() {
MediaViewHolder.create(inflater, parent) MediaViewHolder.create(inflater, parent)
} }
} }

View File

@@ -34,8 +34,8 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -57,10 +57,14 @@ class SeekBarObserverTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
context.orCreateTestableResources context.orCreateTestableResources.addOverride(
.addOverride(R.dimen.qs_media_enabled_seekbar_height, enabledHeight) R.dimen.qs_media_enabled_seekbar_height,
context.orCreateTestableResources enabledHeight
.addOverride(R.dimen.qs_media_disabled_seekbar_height, disabledHeight) )
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_disabled_seekbar_height,
disabledHeight
)
seekBarView = SeekBar(context) seekBarView = SeekBar(context)
seekBarView.progressDrawable = mockSquigglyProgress seekBarView.progressDrawable = mockSquigglyProgress
@@ -70,11 +74,12 @@ class SeekBarObserverTest : SysuiTestCase() {
whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView) whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView) whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
observer = object : SeekBarObserver(mockHolder) { observer =
override fun buildResetAnimator(targetTime: Int): Animator { object : SeekBarObserver(mockHolder) {
return mockSeekbarAnimator override fun buildResetAnimator(targetTime: Int): Animator {
return mockSeekbarAnimator
}
} }
}
} }
@Test @Test

View File

@@ -57,17 +57,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
private lateinit var viewModel: SeekBarViewModel private lateinit var viewModel: SeekBarViewModel
private lateinit var fakeExecutor: FakeExecutor private lateinit var fakeExecutor: FakeExecutor
private val taskExecutor: TaskExecutor = object : TaskExecutor() { private val taskExecutor: TaskExecutor =
override fun executeOnDiskIO(runnable: Runnable) { object : TaskExecutor() {
runnable.run() override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
} }
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
}
@Mock private lateinit var mockController: MediaController @Mock private lateinit var mockController: MediaController
@Mock private lateinit var mockTransport: MediaController.TransportControls @Mock private lateinit var mockTransport: MediaController.TransportControls
@Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingManager: FalsingManager
@@ -81,7 +82,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun setUp() { fun setUp() {
fakeExecutor = FakeExecutor(FakeSystemClock()) fakeExecutor = FakeExecutor(FakeSystemClock())
viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager) viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
viewModel.logSeek = { } viewModel.logSeek = {}
whenever(mockController.sessionToken).thenReturn(token1) whenever(mockController.sessionToken).thenReturn(token1)
whenever(mockBar.context).thenReturn(context) whenever(mockBar.context).thenReturn(context)
@@ -135,16 +136,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationWithPlayback() { fun updateDurationWithPlayback() {
// GIVEN that the duration is contained within the metadata // GIVEN that the duration is contained within the metadata
val duration = 12000L val duration = 12000L
val metadata = MediaMetadata.Builder().run { val metadata =
putLong(MediaMetadata.METADATA_KEY_DURATION, duration) MediaMetadata.Builder().run {
build() putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
} build()
}
whenever(mockController.getMetadata()).thenReturn(metadata) whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed) // AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -158,10 +161,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationWithoutPlayback() { fun updateDurationWithoutPlayback() {
// GIVEN that the duration is contained within the metadata // GIVEN that the duration is contained within the metadata
val duration = 12000L val duration = 12000L
val metadata = MediaMetadata.Builder().run { val metadata =
putLong(MediaMetadata.METADATA_KEY_DURATION, duration) MediaMetadata.Builder().run {
build() putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
} build()
}
whenever(mockController.getMetadata()).thenReturn(metadata) whenever(mockController.getMetadata()).thenReturn(metadata)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -174,16 +178,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationNegative() { fun updateDurationNegative() {
// GIVEN that the duration is negative // GIVEN that the duration is negative
val duration = -1L val duration = -1L
val metadata = MediaMetadata.Builder().run { val metadata =
putLong(MediaMetadata.METADATA_KEY_DURATION, duration) MediaMetadata.Builder().run {
build() putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
} build()
}
whenever(mockController.getMetadata()).thenReturn(metadata) whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed) // AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -195,16 +201,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationZero() { fun updateDurationZero() {
// GIVEN that the duration is zero // GIVEN that the duration is zero
val duration = 0L val duration = 0L
val metadata = MediaMetadata.Builder().run { val metadata =
putLong(MediaMetadata.METADATA_KEY_DURATION, duration) MediaMetadata.Builder().run {
build() putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
} build()
}
whenever(mockController.getMetadata()).thenReturn(metadata) whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed) // AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -218,10 +226,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN that the metadata is null // GIVEN that the metadata is null
whenever(mockController.getMetadata()).thenReturn(null) whenever(mockController.getMetadata()).thenReturn(null)
// AND a valid playback state (ie. media session is not destroyed) // AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -233,10 +242,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateElapsedTime() { fun updateElapsedTime() {
// GIVEN that the PlaybackState contains the current position // GIVEN that the PlaybackState contains the current position
val position = 200L val position = 200L
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, position, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, position, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -248,10 +258,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore @Ignore
fun updateSeekAvailable() { fun updateSeekAvailable() {
// GIVEN that seek is included in actions // GIVEN that seek is included in actions
val state = PlaybackState.Builder().run { val state =
setActions(PlaybackState.ACTION_SEEK_TO) PlaybackState.Builder().run {
build() setActions(PlaybackState.ACTION_SEEK_TO)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -263,10 +274,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore @Ignore
fun updateSeekNotAvailable() { fun updateSeekNotAvailable() {
// GIVEN that seek is not included in actions // GIVEN that seek is not included in actions
val state = PlaybackState.Builder().run { val state =
setActions(PlaybackState.ACTION_PLAY) PlaybackState.Builder().run {
build() setActions(PlaybackState.ACTION_PLAY)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -318,9 +330,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore @Ignore
fun onSeekProgressWithSeekStarting() { fun onSeekProgressWithSeekStarting() {
val pos = 42L val pos = 42L
with(viewModel) { with(viewModel) { onSeekProgress(pos) }
onSeekProgress(pos)
}
fakeExecutor.runAllReady() fakeExecutor.runAllReady()
// THEN then elapsed time should not be updated // THEN then elapsed time should not be updated
assertThat(viewModel.progress.value!!.elapsedTime).isNull() assertThat(viewModel.progress.value!!.elapsedTime).isNull()
@@ -329,11 +339,12 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test @Test
fun seekStarted_listenerNotified() { fun seekStarted_listenerNotified() {
var isScrubbing: Boolean? = null var isScrubbing: Boolean? = null
val listener = object : SeekBarViewModel.ScrubbingChangeListener { val listener =
override fun onScrubbingChanged(scrubbing: Boolean) { object : SeekBarViewModel.ScrubbingChangeListener {
isScrubbing = scrubbing override fun onScrubbingChanged(scrubbing: Boolean) {
isScrubbing = scrubbing
}
} }
}
viewModel.setScrubbingChangeListener(listener) viewModel.setScrubbingChangeListener(listener)
viewModel.onSeekStarting() viewModel.onSeekStarting()
@@ -345,11 +356,12 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test @Test
fun seekEnded_listenerNotified() { fun seekEnded_listenerNotified() {
var isScrubbing: Boolean? = null var isScrubbing: Boolean? = null
val listener = object : SeekBarViewModel.ScrubbingChangeListener { val listener =
override fun onScrubbingChanged(scrubbing: Boolean) { object : SeekBarViewModel.ScrubbingChangeListener {
isScrubbing = scrubbing override fun onScrubbingChanged(scrubbing: Boolean) {
isScrubbing = scrubbing
}
} }
}
viewModel.setScrubbingChangeListener(listener) viewModel.setScrubbingChangeListener(listener)
// Start seeking // Start seeking
@@ -385,9 +397,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
val bar = SeekBar(context) val bar = SeekBar(context)
// WHEN we get an onProgressChanged event without an onStartTrackingTouch event // WHEN we get an onProgressChanged event without an onStartTrackingTouch event
with(viewModel.seekBarListener) { with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) }
onProgressChanged(bar, pos, true)
}
fakeExecutor.runAllReady() fakeExecutor.runAllReady()
// THEN we immediately update the transport // THEN we immediately update the transport
@@ -412,9 +422,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
viewModel.updateController(mockController) viewModel.updateController(mockController)
// WHEN user starts dragging the seek bar // WHEN user starts dragging the seek bar
val pos = 42 val pos = 42
val bar = SeekBar(context).apply { val bar = SeekBar(context).apply { progress = pos }
progress = pos
}
viewModel.seekBarListener.onStartTrackingTouch(bar) viewModel.seekBarListener.onStartTrackingTouch(bar)
fakeExecutor.runAllReady() fakeExecutor.runAllReady()
// THEN transport controls should be used // THEN transport controls should be used
@@ -427,9 +435,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
viewModel.updateController(mockController) viewModel.updateController(mockController)
// WHEN user ends drag // WHEN user ends drag
val pos = 42 val pos = 42
val bar = SeekBar(context).apply { val bar = SeekBar(context).apply { progress = pos }
progress = pos
}
viewModel.seekBarListener.onStopTrackingTouch(bar) viewModel.seekBarListener.onStopTrackingTouch(bar)
fakeExecutor.runAllReady() fakeExecutor.runAllReady()
// THEN transport controls should be used // THEN transport controls should be used
@@ -443,9 +449,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// WHEN user starts dragging the seek bar // WHEN user starts dragging the seek bar
val pos = 42 val pos = 42
val progPos = 84 val progPos = 84
val bar = SeekBar(context).apply { val bar = SeekBar(context).apply { progress = pos }
progress = pos
}
with(viewModel.seekBarListener) { with(viewModel.seekBarListener) {
onStartTrackingTouch(bar) onStartTrackingTouch(bar)
onProgressChanged(bar, progPos, true) onProgressChanged(bar, progPos, true)
@@ -478,10 +482,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test @Test
fun queuePollTaskWhenPlaying() { fun queuePollTaskWhenPlaying() {
// GIVEN that the track is playing // GIVEN that the track is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 100L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 100L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated // WHEN the controller is updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -492,10 +497,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test @Test
fun noQueuePollTaskWhenStopped() { fun noQueuePollTaskWhenStopped() {
// GIVEN that the playback state is stopped // GIVEN that the playback state is stopped
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_STOPPED, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_STOPPED, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN updated // WHEN updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -512,10 +518,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
runAllReady() runAllReady()
} }
// AND the playback state is playing // AND the playback state is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN updated // WHEN updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -532,10 +539,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
runAllReady() runAllReady()
} }
// AND the playback state is playing // AND the playback state is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN updated // WHEN updated
viewModel.updateController(mockController) viewModel.updateController(mockController)
@@ -546,10 +554,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test @Test
fun pollTaskQueuesAnotherPollTaskWhenPlaying() { fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
// GIVEN that the track is playing // GIVEN that the track is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 100L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 100L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController) viewModel.updateController(mockController)
// WHEN the next task runs // WHEN the next task runs
@@ -566,10 +575,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN listening // GIVEN listening
viewModel.listening = true viewModel.listening = true
// AND the playback state is playing // AND the playback state is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController) viewModel.updateController(mockController)
with(fakeExecutor) { with(fakeExecutor) {
@@ -592,10 +602,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN listening // GIVEN listening
viewModel.listening = true viewModel.listening = true
// AND the playback state is playing // AND the playback state is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController) viewModel.updateController(mockController)
with(fakeExecutor) { with(fakeExecutor) {
@@ -621,10 +632,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN listening // GIVEN listening
viewModel.listening = true viewModel.listening = true
// AND the playback state is playing // AND the playback state is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController) viewModel.updateController(mockController)
with(fakeExecutor) { with(fakeExecutor) {
@@ -654,10 +666,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
runAllReady() runAllReady()
} }
// AND the playback state is playing // AND the playback state is playing
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_STOPPED, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_STOPPED, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController) viewModel.updateController(mockController)
// WHEN start listening // WHEN start listening
@@ -673,10 +686,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
verify(mockController).registerCallback(captor.capture()) verify(mockController).registerCallback(captor.capture())
val callback = captor.value val callback = captor.value
// WHEN the callback receives an new state // WHEN the callback receives an new state
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 100L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 100L, 1f)
} build()
}
callback.onPlaybackStateChanged(state) callback.onPlaybackStateChanged(state)
with(fakeExecutor) { with(fakeExecutor) {
advanceClockToNext() advanceClockToNext()
@@ -690,16 +704,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore @Ignore
fun clearSeekBar() { fun clearSeekBar() {
// GIVEN that the duration is contained within the metadata // GIVEN that the duration is contained within the metadata
val metadata = MediaMetadata.Builder().run { val metadata =
putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L) MediaMetadata.Builder().run {
build() putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
} build()
}
whenever(mockController.getMetadata()).thenReturn(metadata) whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed) // AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run { val state =
setState(PlaybackState.STATE_PLAYING, 200L, 1f) PlaybackState.Builder().run {
build() setState(PlaybackState.STATE_PLAYING, 200L, 1f)
} build()
}
whenever(mockController.getPlaybackState()).thenReturn(state) whenever(mockController.getPlaybackState()).thenReturn(state)
// AND the controller has been updated // AND the controller has been updated
viewModel.updateController(mockController) viewModel.updateController(mockController)

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2022 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.
*/
package com.android.systemui.media.controls.models.recommendation package com.android.systemui.media.controls.models.recommendation
import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceAction
@@ -36,11 +52,11 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
@Test @Test
fun isValid_tooFewRecs_returnsFalse() { fun isValid_tooFewRecs_returnsFalse() {
val data = DEFAULT_DATA.copy( val data =
recommendations = listOf( DEFAULT_DATA.copy(
SmartspaceAction.Builder("id", "title").setIcon(icon).build() recommendations =
listOf(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
) )
)
assertThat(data.isValid()).isFalse() assertThat(data.isValid()).isFalse()
} }
@@ -50,14 +66,10 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
val recommendations = mutableListOf<SmartspaceAction>() val recommendations = mutableListOf<SmartspaceAction>()
// Add one fewer recommendation w/ icon than the number required // Add one fewer recommendation w/ icon than the number required
for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) { for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) {
recommendations.add( recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
)
} }
for (i in 1 until 3) { for (i in 1 until 3) {
recommendations.add( recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(null).build())
SmartspaceAction.Builder("id", "title").setIcon(null).build()
)
} }
val data = DEFAULT_DATA.copy(recommendations = recommendations) val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -70,9 +82,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
val recommendations = mutableListOf<SmartspaceAction>() val recommendations = mutableListOf<SmartspaceAction>()
// Add the number of required recommendations // Add the number of required recommendations
for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) { for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) {
recommendations.add( recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
)
} }
val data = DEFAULT_DATA.copy(recommendations = recommendations) val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -85,9 +95,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
val recommendations = mutableListOf<SmartspaceAction>() val recommendations = mutableListOf<SmartspaceAction>()
// Add more than enough recommendations // Add more than enough recommendations
for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) { for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) {
recommendations.add( recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
)
} }
val data = DEFAULT_DATA.copy(recommendations = recommendations) val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -96,13 +104,14 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
} }
} }
private val DEFAULT_DATA = SmartspaceMediaData( private val DEFAULT_DATA =
targetId = "INVALID", SmartspaceMediaData(
isActive = false, targetId = "INVALID",
packageName = "INVALID", isActive = false,
cardAction = null, packageName = "INVALID",
recommendations = emptyList(), cardAction = null,
dismissIntent = null, recommendations = emptyList(),
headphoneConnectionTimeMillis = 0, dismissIntent = null,
instanceId = InstanceId.fakeInstanceId(-1) headphoneConnectionTimeMillis = 0,
) instanceId = InstanceId.fakeInstanceId(-1)
)

View File

@@ -63,24 +63,15 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
@TestableLooper.RunWithLooper @TestableLooper.RunWithLooper
class MediaDataFilterTest : SysuiTestCase() { class MediaDataFilterTest : SysuiTestCase() {
@Mock @Mock private lateinit var listener: MediaDataManager.Listener
private lateinit var listener: MediaDataManager.Listener @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock @Mock private lateinit var broadcastSender: BroadcastSender
private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var mediaDataManager: MediaDataManager
@Mock @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
private lateinit var broadcastSender: BroadcastSender @Mock private lateinit var executor: Executor
@Mock @Mock private lateinit var smartspaceData: SmartspaceMediaData
private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock @Mock private lateinit var logger: MediaUiEventLogger
private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
@Mock
private lateinit var executor: Executor
@Mock
private lateinit var smartspaceData: SmartspaceMediaData
@Mock
private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock
private lateinit var logger: MediaUiEventLogger
private lateinit var mediaDataFilter: MediaDataFilter private lateinit var mediaDataFilter: MediaDataFilter
private lateinit var dataMain: MediaData private lateinit var dataMain: MediaData
@@ -91,14 +82,16 @@ class MediaDataFilterTest : SysuiTestCase() {
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
MediaPlayerData.clear() MediaPlayerData.clear()
mediaDataFilter = MediaDataFilter( mediaDataFilter =
context, MediaDataFilter(
broadcastDispatcher, context,
broadcastSender, broadcastDispatcher,
lockscreenUserManager, broadcastSender,
executor, lockscreenUserManager,
clock, executor,
logger) clock,
logger
)
mediaDataFilter.mediaDataManager = mediaDataManager mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener) mediaDataFilter.addListener(listener)
@@ -106,11 +99,13 @@ class MediaDataFilterTest : SysuiTestCase() {
setUser(USER_MAIN) setUser(USER_MAIN)
// Set up test media data // Set up test media data
dataMain = MediaTestUtils.emptyMediaData.copy( dataMain =
MediaTestUtils.emptyMediaData.copy(
userId = USER_MAIN, userId = USER_MAIN,
packageName = PACKAGE, packageName = PACKAGE,
instanceId = INSTANCE_ID, instanceId = INSTANCE_ID,
appUid = APP_UID) appUid = APP_UID
)
dataGuest = dataMain.copy(userId = USER_GUEST) dataGuest = dataMain.copy(userId = USER_GUEST)
`when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
@@ -118,8 +113,8 @@ class MediaDataFilterTest : SysuiTestCase() {
`when`(smartspaceData.isValid()).thenReturn(true) `when`(smartspaceData.isValid()).thenReturn(true)
`when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE) `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
`when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem)) `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
`when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn( `when`(smartspaceData.headphoneConnectionTimeMillis)
clock.currentTimeMillis() - 100) .thenReturn(clock.currentTimeMillis() - 100)
`when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID) `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
} }
@@ -135,8 +130,8 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
// THEN we should tell the listener // THEN we should tell the listener
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), verify(listener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -145,8 +140,8 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
// THEN we should NOT tell the listener // THEN we should NOT tell the listener
verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(), verify(listener, never())
anyInt(), anyBoolean()) .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
} }
@Test @Test
@@ -192,12 +187,12 @@ class MediaDataFilterTest : SysuiTestCase() {
setUser(USER_GUEST) setUser(USER_GUEST)
// THEN we should add back the guest user media // THEN we should add back the guest user media
verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), verify(listener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
// but not the main user's // but not the main user's
verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), verify(listener, never())
anyInt(), anyBoolean()) .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean())
} }
@Test @Test
@@ -345,7 +340,7 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener) verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
@@ -358,8 +353,8 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(), verify(listener, never())
anyInt(), anyBoolean()) .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
@@ -375,7 +370,7 @@ class MediaDataFilterTest : SysuiTestCase() {
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener) verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
@@ -405,15 +400,15 @@ class MediaDataFilterTest : SysuiTestCase() {
// WHEN we have media that was recently played, but not currently active // WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), verify(listener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
// AND we get a smartspace signal // AND we get a smartspace signal
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
// THEN we should tell listeners to treat the media as not active instead // THEN we should tell listeners to treat the media as not active instead
verify(listener, never()).onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), verify(listener, never())
anyInt(), anyBoolean()) .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse() assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
@@ -428,16 +423,23 @@ class MediaDataFilterTest : SysuiTestCase() {
// WHEN we have media that was recently played, but not currently active // WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), verify(listener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
// AND we get a smartspace signal // AND we get a smartspace signal
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
// THEN we should tell listeners to treat the media as active instead // THEN we should tell listeners to treat the media as active instead
val dataCurrentAndActive = dataCurrent.copy(active = true) val dataCurrentAndActive = dataCurrent.copy(active = true)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), verify(listener)
eq(100), eq(true)) .onMediaDataLoaded(
eq(KEY),
eq(KEY),
eq(dataCurrentAndActive),
eq(true),
eq(100),
eq(true)
)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
// Smartspace update shouldn't be propagated for the empty rec list. // Smartspace update shouldn't be propagated for the empty rec list.
verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
@@ -450,20 +452,27 @@ class MediaDataFilterTest : SysuiTestCase() {
// WHEN we have media that was recently played, but not currently active // WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), verify(listener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
// AND we get a smartspace signal // AND we get a smartspace signal
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
// THEN we should tell listeners to treat the media as active instead // THEN we should tell listeners to treat the media as active instead
val dataCurrentAndActive = dataCurrent.copy(active = true) val dataCurrentAndActive = dataCurrent.copy(active = true)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), verify(listener)
eq(100), eq(true)) .onMediaDataLoaded(
eq(KEY),
eq(KEY),
eq(dataCurrentAndActive),
eq(true),
eq(100),
eq(true)
)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
// Smartspace update should also be propagated but not prioritized. // Smartspace update should also be propagated but not prioritized.
verify(listener) verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
} }
@@ -482,14 +491,21 @@ class MediaDataFilterTest : SysuiTestCase() {
fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() { fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() {
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), verify(listener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
val dataCurrentAndActive = dataCurrent.copy(active = true) val dataCurrentAndActive = dataCurrent.copy(active = true)
verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true), verify(listener)
eq(100), eq(true)) .onMediaDataLoaded(
eq(KEY),
eq(KEY),
eq(dataCurrentAndActive),
eq(true),
eq(100),
eq(true)
)
mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)

View File

@@ -113,7 +113,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
fakeFgExecutor = FakeExecutor(FakeSystemClock()) fakeFgExecutor = FakeExecutor(FakeSystemClock())
fakeBgExecutor = FakeExecutor(FakeSystemClock()) fakeBgExecutor = FakeExecutor(FakeSystemClock())
localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java) localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java)
manager = MediaDeviceManager( manager =
MediaDeviceManager(
context, context,
controllerFactory, controllerFactory,
lmmFactory, lmmFactory,
@@ -124,7 +125,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
fakeFgExecutor, fakeFgExecutor,
fakeBgExecutor, fakeBgExecutor,
dumpster dumpster
) )
manager.addListener(listener) manager.addListener(listener)
// Configure mocks. // Configure mocks.
@@ -138,11 +139,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
// Create a media sesssion and notification for testing. // Create a media sesssion and notification for testing.
session = MediaSession(context, SESSION_KEY) session = MediaSession(context, SESSION_KEY)
mediaData = MediaTestUtils.emptyMediaData.copy( mediaData =
packageName = PACKAGE, MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken)
token = session.sessionToken) whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller)
whenever(controllerFactory.create(session.sessionToken))
.thenReturn(controller)
setupLeAudioConfiguration(false) setupLeAudioConfiguration(false)
} }
@@ -358,7 +357,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
val deviceCallback = captureCallback() val deviceCallback = captureCallback()
// First set a non-null about-to-connect device // First set a non-null about-to-connect device
deviceCallback.onAboutToConnectDeviceAdded( deviceCallback.onAboutToConnectDeviceAdded(
"fakeAddress", "AboutToConnectDeviceName", mock(Drawable::class.java) "fakeAddress",
"AboutToConnectDeviceName",
mock(Drawable::class.java)
) )
// Run and reset the executors and listeners so we only focus on new events. // Run and reset the executors and listeners so we only focus on new events.
fakeBgExecutor.runAllReady() fakeBgExecutor.runAllReady()
@@ -587,8 +588,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test @Test
fun testRemotePlaybackDeviceOverride() { fun testRemotePlaybackDeviceOverride() {
whenever(route.name).thenReturn(DEVICE_NAME) whenever(route.name).thenReturn(DEVICE_NAME)
val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, val deviceData =
showBroadcastButton = false) MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, showBroadcastButton = false)
val mediaDataWithDevice = mediaData.copy(device = deviceData) val mediaDataWithDevice = mediaData.copy(device = deviceData)
// GIVEN media data that already has a device set // GIVEN media data that already has a device set
@@ -617,8 +618,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
val data = captureDeviceData(KEY) val data = captureDeviceData(KEY)
assertThat(data.showBroadcastButton).isTrue() assertThat(data.showBroadcastButton).isTrue()
assertThat(data.enabled).isTrue() assertThat(data.enabled).isTrue()
assertThat(data.name).isEqualTo(context.getString( assertThat(data.name)
R.string.broadcasting_description_is_broadcasting)) .isEqualTo(context.getString(R.string.broadcasting_description_is_broadcasting))
} }
@Test @Test
@@ -659,20 +660,21 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
} }
fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback { fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback { val callback: BluetoothLeBroadcast.Callback =
override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} object : BluetoothLeBroadcast.Callback {
override fun onBroadcastStartFailed(reason: Int) {} override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
override fun onBroadcastStopped(reason: Int, broadcastId: Int) {} override fun onBroadcastStartFailed(reason: Int) {}
override fun onBroadcastStopFailed(reason: Int) {} override fun onBroadcastStopped(reason: Int, broadcastId: Int) {}
override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} override fun onBroadcastStopFailed(reason: Int) {}
override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
override fun onBroadcastMetadataChanged( override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
broadcastId: Int, override fun onBroadcastMetadataChanged(
metadata: BluetoothLeBroadcastMetadata broadcastId: Int,
) {} metadata: BluetoothLeBroadcastMetadata
} ) {}
}
bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback) bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback)
return callback return callback
@@ -681,7 +683,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
fun setupLeAudioConfiguration(isLeAudio: Boolean) { fun setupLeAudioConfiguration(isLeAudio: Boolean) {
whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager) whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
whenever(localBluetoothProfileManager.leAudioBroadcastProfile) whenever(localBluetoothProfileManager.leAudioBroadcastProfile)
.thenReturn(localBluetoothLeBroadcast) .thenReturn(localBluetoothLeBroadcast)
whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio) whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio)
whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME) whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME)
} }
@@ -689,7 +691,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
fun setupBroadcastPackage(currentName: String) { fun setupBroadcastPackage(currentName: String) {
whenever(lmm.packageName).thenReturn(PACKAGE) whenever(lmm.packageName).thenReturn(PACKAGE)
whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt())) whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
.thenReturn(applicationInfo) .thenReturn(applicationInfo)
whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName) whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName)
context.setMockPackageManager(packageManager) context.setMockPackageManager(packageManager)
} }

View File

@@ -23,14 +23,12 @@ import android.media.session.MediaSessionManager
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@@ -44,17 +42,15 @@ import org.mockito.Mockito.any
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
private const val PACKAGE = "PKG" private const val PACKAGE = "PKG"
private const val KEY = "TEST_KEY" private const val KEY = "TEST_KEY"
private const val NOTIF_KEY = "TEST_KEY" private const val NOTIF_KEY = "TEST_KEY"
private val info = MediaTestUtils.emptyMediaData.copy( private val info =
packageName = PACKAGE, MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY)
notificationKey = NOTIF_KEY
)
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -141,10 +137,10 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
// Capture listener // Capture listener
bgExecutor.runAllReady() bgExecutor.runAllReady()
val listenerCaptor = ArgumentCaptor.forClass( val listenerCaptor =
MediaSessionManager.OnActiveSessionsChangedListener::class.java) ArgumentCaptor.forClass(MediaSessionManager.OnActiveSessionsChangedListener::class.java)
verify(mediaSessionManager).addOnActiveSessionsChangedListener( verify(mediaSessionManager)
listenerCaptor.capture(), any()) .addOnActiveSessionsChangedListener(listenerCaptor.capture(), any())
sessionListener = listenerCaptor.value sessionListener = listenerCaptor.value
filter.addListener(mediaListener) filter.addListener(mediaListener)
@@ -163,8 +159,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
filter.onMediaDataLoaded(KEY, null, mediaData1) filter.onMediaDataLoaded(KEY, null, mediaData1)
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -186,8 +182,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -216,8 +212,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -232,15 +228,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the local session // WHEN a loaded event is received that matches the local session
filter.onMediaDataLoaded(KEY, null, mediaData2) filter.onMediaDataLoaded(KEY, null, mediaData2)
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is filtered // THEN the event is filtered
verify(mediaListener, never()).onMediaDataLoaded( verify(mediaListener, never())
eq(KEY), eq(null), eq(mediaData2), anyBoolean(), anyInt(), anyBoolean()) .onMediaDataLoaded(
eq(KEY),
eq(null),
eq(mediaData2),
anyBoolean(),
anyInt(),
anyBoolean()
)
} }
@Test @Test
@@ -256,8 +259,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered because there isn't a notification for the remote // THEN the event is not filtered because there isn't a notification for the remote
// session. // session.
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -274,16 +277,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the local session // WHEN a loaded event is received that matches the local session
filter.onMediaDataLoaded(key2, null, mediaData2) filter.onMediaDataLoaded(key2, null, mediaData2)
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is filtered // THEN the event is filtered
verify(mediaListener, never()) verify(mediaListener, never())
.onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(), .onMediaDataLoaded(
anyInt(), anyBoolean()) eq(key2),
eq(null),
eq(mediaData2),
anyBoolean(),
anyInt(),
anyBoolean()
)
// AND there should be a removed event for key2 // AND there should be a removed event for key2
verify(mediaListener).onMediaDataRemoved(eq(key2)) verify(mediaListener).onMediaDataRemoved(eq(key2))
} }
@@ -302,15 +311,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the remote session // WHEN a loaded event is received that matches the remote session
filter.onMediaDataLoaded(key2, null, mediaData2) filter.onMediaDataLoaded(key2, null, mediaData2)
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -326,15 +335,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the local session // WHEN a loaded event is received that matches the local session
filter.onMediaDataLoaded(KEY, null, mediaData2) filter.onMediaDataLoaded(KEY, null, mediaData2)
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -352,8 +361,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the event is not filtered // THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -375,8 +384,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the key migration event is fired // THEN the key migration event is fired
verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), eq(0), eq(false))
} }
@Test @Test
@@ -406,14 +415,20 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the key migration event is filtered // THEN the key migration event is filtered
verify(mediaListener, never()) verify(mediaListener, never())
.onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(), .onMediaDataLoaded(
anyInt(), anyBoolean()) eq(key2),
eq(null),
eq(mediaData2),
anyBoolean(),
anyInt(),
anyBoolean()
)
// WHEN a loaded event is received that matches the remote session // WHEN a loaded event is received that matches the remote session
filter.onMediaDataLoaded(key2, null, mediaData1) filter.onMediaDataLoaded(key2, null, mediaData1)
bgExecutor.runAllReady() bgExecutor.runAllReady()
fgExecutor.runAllReady() fgExecutor.runAllReady()
// THEN the key migration event is fired // THEN the key migration event is fired
verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), verify(mediaListener)
eq(0), eq(false)) .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
} }
} }

View File

@@ -44,11 +44,11 @@ import org.mockito.ArgumentMatchers.anyString
import org.mockito.Captor import org.mockito.Captor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY" private const val KEY = "KEY"
@@ -73,7 +73,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback> @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
@Captor private lateinit var dozingCallbackCaptor: @Captor
private lateinit var dozingCallbackCaptor:
ArgumentCaptor<StatusBarStateController.StateListener> ArgumentCaptor<StatusBarStateController.StateListener>
@JvmField @Rule val mockito = MockitoJUnit.rule() @JvmField @Rule val mockito = MockitoJUnit.rule()
private lateinit var metadataBuilder: MediaMetadata.Builder private lateinit var metadataBuilder: MediaMetadata.Builder
@@ -88,36 +89,41 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun setup() { fun setup() {
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController) `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
executor = FakeExecutor(clock) executor = FakeExecutor(clock)
mediaTimeoutListener = MediaTimeoutListener( mediaTimeoutListener =
mediaControllerFactory, MediaTimeoutListener(
executor, mediaControllerFactory,
logger, executor,
statusBarStateController, logger,
clock statusBarStateController,
) clock
)
mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback mediaTimeoutListener.stateCallback = stateCallback
// Create a media session and notification for testing. // Create a media session and notification for testing.
metadataBuilder = MediaMetadata.Builder().apply { metadataBuilder =
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) MediaMetadata.Builder().apply {
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
} putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
playbackBuilder = PlaybackState.Builder().apply { }
setState(PlaybackState.STATE_PAUSED, 6000L, 1f) playbackBuilder =
setActions(PlaybackState.ACTION_PLAY) PlaybackState.Builder().apply {
} setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
session = MediaSession(context, SESSION_KEY).apply { setActions(PlaybackState.ACTION_PLAY)
setMetadata(metadataBuilder.build()) }
setPlaybackState(playbackBuilder.build()) session =
} MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
session.setActive(true) session.setActive(true)
mediaData = MediaTestUtils.emptyMediaData.copy( mediaData =
app = PACKAGE, MediaTestUtils.emptyMediaData.copy(
packageName = PACKAGE, app = PACKAGE,
token = session.sessionToken packageName = PACKAGE,
) token = session.sessionToken
)
resumeData = mediaData.copy(token = null, active = false, resumption = true) resumeData = mediaData.copy(token = null, active = false, resumption = true)
} }
@@ -215,8 +221,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we're registered // Assuming we're registered
testOnMediaDataLoaded_registersPlaybackListener() testOnMediaDataLoaded_registersPlaybackListener()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() mediaCallbackCaptor.value.onPlaybackStateChanged(
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
} }
@@ -226,8 +233,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we have a pending timeout // Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused() testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() mediaCallbackCaptor.value.onPlaybackStateChanged(
.setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()) PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
)
assertThat(executor.numPending()).isEqualTo(0) assertThat(executor.numPending()).isEqualTo(0)
verify(logger).logTimeoutCancelled(eq(KEY), any()) verify(logger).logTimeoutCancelled(eq(KEY), any())
} }
@@ -237,8 +245,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we have a pending timeout // Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused() testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() mediaCallbackCaptor.value.onPlaybackStateChanged(
.setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()) PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
)
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
} }
@@ -332,9 +341,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() { fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
// WHEN regular media is paused // WHEN regular media is paused
val pausedState = PlaybackState.Builder() val pausedState =
.setState(PlaybackState.STATE_PAUSED, 0L, 0f) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
.build()
`when`(mediaController.playbackState).thenReturn(pausedState) `when`(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
@@ -365,9 +373,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
// AND that media is resumed // AND that media is resumed
val playingState = PlaybackState.Builder() val playingState =
.setState(PlaybackState.STATE_PAUSED, 0L, 0f) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
.build()
`when`(mediaController.playbackState).thenReturn(playingState) `when`(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData) mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
@@ -389,15 +396,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() { fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
// Load media data once // Load media data once
val pausedState = PlaybackState.Builder() val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
.setActions(PlaybackState.ACTION_PAUSE)
.build()
loadMediaDataWithPlaybackState(pausedState) loadMediaDataWithPlaybackState(pausedState)
// When media data is loaded again, with different actions // When media data is loaded again, with different actions
val playingState = PlaybackState.Builder() val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
.setActions(PlaybackState.ACTION_PLAY)
.build()
loadMediaDataWithPlaybackState(playingState) loadMediaDataWithPlaybackState(playingState)
// Then the callback is not invoked // Then the callback is not invoked
@@ -407,15 +410,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() { fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
// Load media data once // Load media data once
val pausedState = PlaybackState.Builder() val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
.setActions(PlaybackState.ACTION_PAUSE)
.build()
loadMediaDataWithPlaybackState(pausedState) loadMediaDataWithPlaybackState(pausedState)
// When the playback state changes, and has different actions // When the playback state changes, and has different actions
val playingState = PlaybackState.Builder() val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
.setActions(PlaybackState.ACTION_PLAY)
.build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
// Then the callback is invoked // Then the callback is invoked
@@ -424,24 +423,30 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() { fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
val customOne = PlaybackState.CustomAction.Builder( val customOne =
PlaybackState.CustomAction.Builder(
"ACTION_1", "ACTION_1",
"custom action 1", "custom action 1",
android.R.drawable.ic_media_ff) android.R.drawable.ic_media_ff
)
.build() .build()
val pausedState = PlaybackState.Builder() val pausedState =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE) .setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customOne) .addCustomAction(customOne)
.build() .build()
loadMediaDataWithPlaybackState(pausedState) loadMediaDataWithPlaybackState(pausedState)
// When the playback state actions change // When the playback state actions change
val customTwo = PlaybackState.CustomAction.Builder( val customTwo =
"ACTION_2", PlaybackState.CustomAction.Builder(
"custom action 2", "ACTION_2",
android.R.drawable.ic_media_rew) "custom action 2",
android.R.drawable.ic_media_rew
)
.build() .build()
val pausedStateTwoActions = PlaybackState.Builder() val pausedStateTwoActions =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE) .setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customOne) .addCustomAction(customOne)
.addCustomAction(customTwo) .addCustomAction(customTwo)
@@ -454,9 +459,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnPlaybackStateChanged_sameActions_noCallback() { fun testOnPlaybackStateChanged_sameActions_noCallback() {
val stateWithActions = PlaybackState.Builder() val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
.setActions(PlaybackState.ACTION_PLAY)
.build()
loadMediaDataWithPlaybackState(stateWithActions) loadMediaDataWithPlaybackState(stateWithActions)
// When the playback state updates with the same actions // When the playback state updates with the same actions
@@ -470,18 +473,20 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun testOnPlaybackStateChanged_sameCustomActions_noCallback() { fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
val actionName = "custom action" val actionName = "custom action"
val actionIcon = android.R.drawable.ic_media_ff val actionIcon = android.R.drawable.ic_media_ff
val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon) val customOne =
.build() PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
val stateOne = PlaybackState.Builder() val stateOne =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE) .setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customOne) .addCustomAction(customOne)
.build() .build()
loadMediaDataWithPlaybackState(stateOne) loadMediaDataWithPlaybackState(stateOne)
// When the playback state is updated, but has the same actions // When the playback state is updated, but has the same actions
val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon) val customTwo =
.build() PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
val stateTwo = PlaybackState.Builder() val stateTwo =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE) .setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customTwo) .addCustomAction(customTwo)
.build() .build()
@@ -494,15 +499,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnMediaDataLoaded_isPlayingChanged_noCallback() { fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
// Load media data in paused state // Load media data in paused state
val pausedState = PlaybackState.Builder() val pausedState =
.setState(PlaybackState.STATE_PAUSED, 0L, 0f) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
.build()
loadMediaDataWithPlaybackState(pausedState) loadMediaDataWithPlaybackState(pausedState)
// When media data is loaded again but playing // When media data is loaded again but playing
val playingState = PlaybackState.Builder() val playingState =
.setState(PlaybackState.STATE_PLAYING, 0L, 1f) PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
.build()
loadMediaDataWithPlaybackState(playingState) loadMediaDataWithPlaybackState(playingState)
// Then the callback is not invoked // Then the callback is not invoked
@@ -512,15 +515,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() { fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
// Load media data in paused state // Load media data in paused state
val pausedState = PlaybackState.Builder() val pausedState =
.setState(PlaybackState.STATE_PAUSED, 0L, 0f) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
.build()
loadMediaDataWithPlaybackState(pausedState) loadMediaDataWithPlaybackState(pausedState)
// When the playback state changes to playing // When the playback state changes to playing
val playingState = PlaybackState.Builder() val playingState =
.setState(PlaybackState.STATE_PLAYING, 0L, 1f) PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
.build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
// Then the callback is invoked // Then the callback is invoked
@@ -530,15 +531,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnPlaybackStateChanged_isPlayingSame_noCallback() { fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
// Load media data in paused state // Load media data in paused state
val pausedState = PlaybackState.Builder() val pausedState =
.setState(PlaybackState.STATE_PAUSED, 0L, 0f) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
.build()
loadMediaDataWithPlaybackState(pausedState) loadMediaDataWithPlaybackState(pausedState)
// When the playback state is updated, but still not playing // When the playback state is updated, but still not playing
val playingState = PlaybackState.Builder() val playingState =
.setState(PlaybackState.STATE_STOPPED, 0L, 0f) PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
.build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
// Then the callback is not invoked // Then the callback is not invoked
@@ -549,8 +548,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
// When paused media is loaded // When paused media is loaded
testOnMediaDataLoaded_registersPlaybackListener() testOnMediaDataLoaded_registersPlaybackListener()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() mediaCallbackCaptor.value.onPlaybackStateChanged(
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
// And we doze past the scheduled timeout // And we doze past the scheduled timeout
@@ -574,8 +574,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
val time = clock.currentTimeMillis() val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time) clock.setElapsedRealtime(time)
testOnMediaDataLoaded_registersPlaybackListener() testOnMediaDataLoaded_registersPlaybackListener()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() mediaCallbackCaptor.value.onPlaybackStateChanged(
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
// And we doze, but not past the scheduled timeout // And we doze, but not past the scheduled timeout

View File

@@ -36,13 +36,13 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
import com.android.systemui.tuner.TunerService import com.android.systemui.tuner.TunerService
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import org.junit.After
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -68,7 +68,9 @@ private const val MEDIA_PREFERENCES = "media_control_prefs"
private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3" private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
private fun <T> any(): T = Mockito.any<T>() private fun <T> any(): T = Mockito.any<T>()
@SmallTest @SmallTest
@@ -98,26 +100,32 @@ class MediaResumeListenerTest : SysuiTestCase() {
private lateinit var resumeListener: MediaResumeListener private lateinit var resumeListener: MediaResumeListener
private val clock = FakeSystemClock() private val clock = FakeSystemClock()
private var originalQsSetting = Settings.Global.getInt(context.contentResolver, private var originalQsSetting =
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) Settings.Global.getInt(
private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver, context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME, 0) Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
1
)
private var originalResumeSetting =
Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
@Before @Before
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
Settings.Global.putInt(context.contentResolver, Settings.Global.putInt(
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) context.contentResolver,
Settings.Secure.putInt(context.contentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
Settings.Secure.MEDIA_CONTROLS_RESUME, 1) 1
)
Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
whenever(resumeBrowserFactory.create(capture(callbackCaptor), any())) whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
.thenReturn(resumeBrowser) .thenReturn(resumeBrowser)
// resume components are stored in sharedpreferences // resume components are stored in sharedpreferences
whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt())) whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
.thenReturn(sharedPrefs) .thenReturn(sharedPrefs)
whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS) whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor) whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor) whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
@@ -125,36 +133,59 @@ class MediaResumeListenerTest : SysuiTestCase() {
whenever(mockContext.contentResolver).thenReturn(context.contentResolver) whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
executor = FakeExecutor(clock) executor = FakeExecutor(clock)
resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, resumeListener =
tunerService, resumeBrowserFactory, dumpManager, clock) MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager) resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener) mediaDataManager.addListener(resumeListener)
data = MediaTestUtils.emptyMediaData.copy( data =
MediaTestUtils.emptyMediaData.copy(
song = TITLE, song = TITLE,
packageName = PACKAGE_NAME, packageName = PACKAGE_NAME,
token = token) token = token
)
} }
@After @After
fun tearDown() { fun tearDown() {
Settings.Global.putInt(context.contentResolver, Settings.Global.putInt(
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting) context.contentResolver,
Settings.Secure.putInt(context.contentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting) originalQsSetting
)
Settings.Secure.putInt(
context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME,
originalResumeSetting
)
} }
@Test @Test
fun testWhenNoResumption_doesNothing() { fun testWhenNoResumption_doesNothing() {
Settings.Secure.putInt(context.contentResolver, Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
// When listener is created, we do NOT register a user change listener // When listener is created, we do NOT register a user change listener
val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService, val listener =
resumeBrowserFactory, dumpManager, clock) MediaResumeListener(
context,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
listener.setManager(mediaDataManager) listener.setManager(mediaDataManager)
verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver), verify(broadcastDispatcher, never())
any(), any(), any(), anyInt(), any()) .registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any())
// When data is loaded, we do NOT execute or update anything // When data is loaded, we do NOT execute or update anything
listener.onMediaDataLoaded(KEY, OLD_KEY, data) listener.onMediaDataLoaded(KEY, OLD_KEY, data)
@@ -175,9 +206,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
fun testOnLoad_checksForResume_badService() { fun testOnLoad_checksForResume_badService() {
setUpMbsWithValidResolveInfo() setUpMbsWithValidResolveInfo()
whenever(resumeBrowser.testConnection()).thenAnswer { whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
callbackCaptor.value.onError()
}
// When media data is loaded that has not been checked yet, and does not have a MBS // When media data is loaded that has not been checked yet, and does not have a MBS
resumeListener.onMediaDataLoaded(KEY, null, data) resumeListener.onMediaDataLoaded(KEY, null, data)
@@ -231,7 +260,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
// But we do not tell it to add new controls // But we do not tell it to add new controls
verify(mediaDataManager, never()) verify(mediaDataManager, never())
.addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any()) .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
} }
@Test @Test
@@ -258,8 +287,15 @@ class MediaResumeListenerTest : SysuiTestCase() {
// Make sure broadcast receiver is registered // Make sure broadcast receiver is registered
resumeListener.setManager(mediaDataManager) resumeListener.setManager(mediaDataManager)
verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver), verify(broadcastDispatcher)
any(), any(), any(), anyInt(), any()) .registerReceiver(
eq(resumeListener.userChangeReceiver),
any(),
any(),
any(),
anyInt(),
any()
)
// When we get an unlock event // When we get an unlock event
val intent = Intent(Intent.ACTION_USER_UNLOCKED) val intent = Intent(Intent.ACTION_USER_UNLOCKED)
@@ -269,8 +305,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
verify(resumeBrowser, times(3)).findRecentMedia() verify(resumeBrowser, times(3)).findRecentMedia()
// Then since the mock service found media, the manager should be informed // Then since the mock service found media, the manager should be informed
verify(mediaDataManager, times(3)).addResumptionControls(anyInt(), verify(mediaDataManager, times(3))
any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
} }
@Test @Test
@@ -309,12 +345,14 @@ class MediaResumeListenerTest : SysuiTestCase() {
// Then we save an update with the current time // Then we save an update with the current time
verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) componentCaptor.value
.dropLastWhile { it.isEmpty() }.forEach { .split(ResumeMediaBrowser.DELIMITER.toRegex())
val result = it.split("/") .dropLastWhile { it.isEmpty() }
assertThat(result.size).isEqualTo(3) .forEach {
assertThat(result[2].toLong()).isEqualTo(currentTime) val result = it.split("/")
} assertThat(result.size).isEqualTo(3)
assertThat(result[2].toLong()).isEqualTo(currentTime)
}
verify(sharedPrefsEditor, times(1)).apply() verify(sharedPrefsEditor, times(1)).apply()
} }
@@ -333,8 +371,16 @@ class MediaResumeListenerTest : SysuiTestCase() {
val lastPlayed = clock.currentTimeMillis() val lastPlayed = clock.currentTimeMillis()
val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, val resumeListener =
tunerService, resumeBrowserFactory, dumpManager, clock) MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager) resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener) mediaDataManager.addListener(resumeListener)
@@ -344,8 +390,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
// We add its resume controls // We add its resume controls
verify(resumeBrowser, times(1)).findRecentMedia() verify(resumeBrowser, times(1)).findRecentMedia()
verify(mediaDataManager, times(1)).addResumptionControls(anyInt(), verify(mediaDataManager, times(1))
any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
} }
@Test @Test
@@ -354,8 +400,16 @@ class MediaResumeListenerTest : SysuiTestCase() {
val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100 val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, val resumeListener =
tunerService, resumeBrowserFactory, dumpManager, clock) MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager) resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener) mediaDataManager.addListener(resumeListener)
@@ -365,8 +419,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
// We do not try to add resume controls // We do not try to add resume controls
verify(resumeBrowser, times(0)).findRecentMedia() verify(resumeBrowser, times(0)).findRecentMedia()
verify(mediaDataManager, times(0)).addResumptionControls(anyInt(), verify(mediaDataManager, times(0))
any(), any(), any(), any(), any(), any()) .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
} }
@Test @Test
@@ -385,8 +439,16 @@ class MediaResumeListenerTest : SysuiTestCase() {
val lastPlayed = currentTime - 1000 val lastPlayed = currentTime - 1000
val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, val resumeListener =
tunerService, resumeBrowserFactory, dumpManager, clock) MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager) resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener) mediaDataManager.addListener(resumeListener)
@@ -396,12 +458,14 @@ class MediaResumeListenerTest : SysuiTestCase() {
// Then we store the new lastPlayed time // Then we store the new lastPlayed time
verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) componentCaptor.value
.dropLastWhile { it.isEmpty() }.forEach { .split(ResumeMediaBrowser.DELIMITER.toRegex())
val result = it.split("/") .dropLastWhile { it.isEmpty() }
assertThat(result.size).isEqualTo(3) .forEach {
assertThat(result[2].toLong()).isEqualTo(currentTime) val result = it.split("/")
} assertThat(result.size).isEqualTo(3)
assertThat(result[2].toLong()).isEqualTo(currentTime)
}
verify(sharedPrefsEditor, times(1)).apply() verify(sharedPrefsEditor, times(1)).apply()
} }
@@ -422,9 +486,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
setUpMbsWithValidResolveInfo() setUpMbsWithValidResolveInfo()
// Set up mocks to return with an error // Set up mocks to return with an error
whenever(resumeBrowser.testConnection()).thenAnswer { whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
callbackCaptor.value.onError()
}
resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data) resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
executor.runAllReady() executor.runAllReady()

View File

@@ -37,8 +37,8 @@ import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
private const val PACKAGE_NAME = "package" private const val PACKAGE_NAME = "package"
private const val CLASS_NAME = "class" private const val CLASS_NAME = "class"
@@ -47,7 +47,9 @@ private const val MEDIA_ID = "media ID"
private const val ROOT = "media browser root" private const val ROOT = "media browser root"
private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
private fun <T> any(): T = Mockito.any<T>() private fun <T> any(): T = Mockito.any<T>()
@SmallTest @SmallTest
@@ -57,10 +59,8 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
private lateinit var resumeBrowser: TestableResumeMediaBrowser private lateinit var resumeBrowser: TestableResumeMediaBrowser
private val component = ComponentName(PACKAGE_NAME, CLASS_NAME) private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
private val description = MediaDescription.Builder() private val description =
.setTitle(TITLE) MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build()
.setMediaId(MEDIA_ID)
.build()
@Mock lateinit var callback: ResumeMediaBrowser.Callback @Mock lateinit var callback: ResumeMediaBrowser.Callback
@Mock lateinit var listener: MediaResumeListener @Mock lateinit var listener: MediaResumeListener
@@ -81,19 +81,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(browserFactory.create(any(), capture(connectionCallback), any())) whenever(browserFactory.create(any(), capture(connectionCallback), any()))
.thenReturn(browser) .thenReturn(browser)
whenever(mediaController.transportControls).thenReturn(transportControls) whenever(mediaController.transportControls).thenReturn(transportControls)
whenever(mediaController.sessionToken).thenReturn(token) whenever(mediaController.sessionToken).thenReturn(token)
resumeBrowser = TestableResumeMediaBrowser( resumeBrowser =
context, TestableResumeMediaBrowser(
callback, context,
component, callback,
browserFactory, component,
logger, browserFactory,
mediaController logger,
) mediaController
)
} }
@Test @Test
@@ -329,30 +330,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
verify(oldBrowser).disconnect() verify(oldBrowser).disconnect()
} }
/** /** Helper function to mock a failed connection */
* Helper function to mock a failed connection
*/
private fun setupBrowserFailed() { private fun setupBrowserFailed() {
whenever(browser.connect()).thenAnswer { whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() }
connectionCallback.value.onConnectionFailed()
}
} }
/** /** Helper function to mock a successful connection only */
* Helper function to mock a successful connection only
*/
private fun setupBrowserConnection() { private fun setupBrowserConnection() {
whenever(browser.connect()).thenAnswer { whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() }
connectionCallback.value.onConnected()
}
whenever(browser.isConnected()).thenReturn(true) whenever(browser.isConnected()).thenReturn(true)
whenever(browser.getRoot()).thenReturn(ROOT) whenever(browser.getRoot()).thenReturn(ROOT)
whenever(browser.sessionToken).thenReturn(token) whenever(browser.sessionToken).thenReturn(token)
} }
/** /** Helper function to mock a successful connection, but no media results */
* Helper function to mock a successful connection, but no media results
*/
private fun setupBrowserConnectionNoResults() { private fun setupBrowserConnectionNoResults() {
setupBrowserConnection() setupBrowserConnection()
whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer { whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
@@ -360,9 +351,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
} }
} }
/** /** Helper function to mock a successful connection, but no playable results */
* Helper function to mock a successful connection, but no playable results
*/
private fun setupBrowserConnectionNotPlayable() { private fun setupBrowserConnectionNotPlayable() {
setupBrowserConnection() setupBrowserConnection()
@@ -373,9 +362,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
} }
} }
/** /** Helper function to mock a successful connection with playable media */
* Helper function to mock a successful connection with playable media
*/
private fun setupBrowserConnectionValidMedia() { private fun setupBrowserConnectionValidMedia() {
setupBrowserConnection() setupBrowserConnection()
@@ -387,9 +374,7 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
} }
} }
/** /** Override so media controller use is testable */
* Override so media controller use is testable
*/
private class TestableResumeMediaBrowser( private class TestableResumeMediaBrowser(
context: Context, context: Context,
callback: Callback, callback: Callback,
@@ -403,4 +388,4 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
return fakeController return fakeController
} }
} }
} }

View File

@@ -16,24 +16,24 @@
package com.android.systemui.media.controls.ui package com.android.systemui.media.controls.ui
import org.mockito.Mockito.`when` as whenever
import android.graphics.drawable.Animatable2 import android.graphics.drawable.Animatable2
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.test.suitebuilder.annotation.SmallTest import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import junit.framework.Assert.assertTrue
import junit.framework.Assert.assertFalse import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.times
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
@SmallTest @SmallTest
@@ -56,8 +56,7 @@ class AnimationBindHandlerTest : SysuiTestCase() {
handler = AnimationBindHandler() handler = AnimationBindHandler()
} }
@After @After fun tearDown() {}
fun tearDown() {}
@Test @Test
fun registerNoAnimations_executeCallbackImmediately() { fun registerNoAnimations_executeCallbackImmediately() {

View File

@@ -69,21 +69,18 @@ class ColorSchemeTransitionTest : SysuiTestCase() {
animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition } animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition }
whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR) whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR)
colorSchemeTransition = ColorSchemeTransition( colorSchemeTransition =
context, mediaViewHolder, animatingColorTransitionFactory ColorSchemeTransition(context, mediaViewHolder, animatingColorTransitionFactory)
)
colorTransition = object : AnimatingColorTransition( colorTransition =
DEFAULT_COLOR, extractColor, applyColor object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) {
) { override fun buildAnimator(): ValueAnimator {
override fun buildAnimator(): ValueAnimator { return valueAnimator
return valueAnimator }
} }
}
} }
@After @After fun tearDown() {}
fun tearDown() {}
@Test @Test
fun testColorTransition_nullColorScheme_keepsDefault() { fun testColorTransition_nullColorScheme_keepsDefault() {

View File

@@ -48,17 +48,12 @@ import org.mockito.junit.MockitoJUnit
@TestableLooper.RunWithLooper @TestableLooper.RunWithLooper
class KeyguardMediaControllerTest : SysuiTestCase() { class KeyguardMediaControllerTest : SysuiTestCase() {
@Mock @Mock private lateinit var mediaHost: MediaHost
private lateinit var mediaHost: MediaHost @Mock private lateinit var bypassController: KeyguardBypassController
@Mock @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
private lateinit var bypassController: KeyguardBypassController @Mock private lateinit var configurationController: ConfigurationController
@Mock
private lateinit var statusBarStateController: SysuiStatusBarStateController
@Mock
private lateinit var configurationController: ConfigurationController
@JvmField @Rule @JvmField @Rule val mockito = MockitoJUnit.rule()
val mockito = MockitoJUnit.rule()
private val mediaContainerView: MediaContainerView = MediaContainerView(context, null) private val mediaContainerView: MediaContainerView = MediaContainerView(context, null)
private val hostView = UniqueObjectHostView(context) private val hostView = UniqueObjectHostView(context)
@@ -76,15 +71,16 @@ class KeyguardMediaControllerTest : SysuiTestCase() {
hostView.layoutParams = FrameLayout.LayoutParams(100, 100) hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
testableLooper = TestableLooper.get(this) testableLooper = TestableLooper.get(this)
fakeHandler = FakeHandler(testableLooper.looper) fakeHandler = FakeHandler(testableLooper.looper)
keyguardMediaController = KeyguardMediaController( keyguardMediaController =
mediaHost, KeyguardMediaController(
bypassController, mediaHost,
statusBarStateController, bypassController,
context, statusBarStateController,
settings, context,
fakeHandler, settings,
configurationController, fakeHandler,
) configurationController,
)
keyguardMediaController.attachSinglePaneContainer(mediaContainerView) keyguardMediaController.attachSinglePaneContainer(mediaContainerView)
keyguardMediaController.useSplitShade = false keyguardMediaController.useSplitShade = false
} }
@@ -153,8 +149,10 @@ class KeyguardMediaControllerTest : SysuiTestCase() {
keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
keyguardMediaController.useSplitShade = true keyguardMediaController.useSplitShade = true
assertTrue("HostView wasn't attached to the split pane container", assertTrue(
splitShadeContainer.childCount == 1) "HostView wasn't attached to the split pane container",
splitShadeContainer.childCount == 1
)
} }
@Test @Test
@@ -162,8 +160,10 @@ class KeyguardMediaControllerTest : SysuiTestCase() {
val splitShadeContainer = FrameLayout(context) val splitShadeContainer = FrameLayout(context)
keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
assertTrue("HostView wasn't attached to the single pane container", assertTrue(
mediaContainerView.childCount == 1) "HostView wasn't attached to the single pane container",
mediaContainerView.childCount == 1
)
} }
@Test @Test

View File

@@ -93,22 +93,23 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Before @Before
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
mediaCarouselController = MediaCarouselController( mediaCarouselController =
context, MediaCarouselController(
mediaControlPanelFactory, context,
visualStabilityProvider, mediaControlPanelFactory,
mediaHostStatesManager, visualStabilityProvider,
activityStarter, mediaHostStatesManager,
clock, activityStarter,
executor, clock,
mediaDataManager, executor,
configurationController, mediaDataManager,
falsingCollector, configurationController,
falsingManager, falsingCollector,
dumpManager, falsingManager,
logger, dumpManager,
debugLogger logger,
) debugLogger
)
verify(mediaDataManager).addListener(capture(listener)) verify(mediaDataManager).addListener(capture(listener))
verify(visualStabilityProvider) verify(visualStabilityProvider)
.addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
@@ -121,60 +122,139 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test @Test
fun testPlayerOrdering() { fun testPlayerOrdering() {
// Test values: key, data, last active time // Test values: key, data, last active time
val playingLocal = Triple("playing local", val playingLocal =
DATA.copy(active = true, isPlaying = true, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), "playing local",
4500L) DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
),
4500L
)
val playingCast = Triple("playing cast", val playingCast =
DATA.copy(active = true, isPlaying = true, Triple(
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false), "playing cast",
5000L) DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
resumption = false
),
5000L
)
val pausedLocal = Triple("paused local", val pausedLocal =
DATA.copy(active = true, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false), "paused local",
1000L) DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
),
1000L
)
val pausedCast = Triple("paused cast", val pausedCast =
DATA.copy(active = true, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false), "paused cast",
2000L) DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
resumption = false
),
2000L
)
val playingRcn = Triple("playing RCN", val playingRcn =
DATA.copy(active = true, isPlaying = true, Triple(
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false), "playing RCN",
5000L) DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
resumption = false
),
5000L
)
val pausedRcn = Triple("paused RCN", val pausedRcn =
DATA.copy(active = true, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false), "paused RCN",
5000L) DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
resumption = false
),
5000L
)
val active = Triple("active", val active =
DATA.copy(active = true, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), "active",
250L) DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
),
250L
)
val resume1 = Triple("resume 1", val resume1 =
DATA.copy(active = false, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), "resume 1",
500L) DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
),
500L
)
val resume2 = Triple("resume 2", val resume2 =
DATA.copy(active = false, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true), "resume 2",
1000L) DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
),
1000L
)
val activeMoreRecent = Triple("active more recent", val activeMoreRecent =
DATA.copy(active = false, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L), "active more recent",
1000L) DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true,
lastActive = 2L
),
1000L
)
val activeLessRecent = Triple("active less recent", val activeLessRecent =
DATA.copy(active = false, isPlaying = false, Triple(
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L), "active less recent",
1000L) DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true,
lastActive = 1L
),
1000L
)
// Expected ordering for media players: // Expected ordering for media players:
// Actively playing local sessions // Actively playing local sessions
// Actively playing cast sessions // Actively playing cast sessions
@@ -182,13 +262,28 @@ class MediaCarouselControllerTest : SysuiTestCase() {
// RCNs // RCNs
// Resume controls, by last active // Resume controls, by last active
val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn, val expected =
pausedRcn, active, resume2, resume1) listOf(
playingLocal,
playingCast,
pausedCast,
pausedLocal,
playingRcn,
pausedRcn,
active,
resume2,
resume1
)
expected.forEach { expected.forEach {
clock.setCurrentTimeMillis(it.third) clock.setCurrentTimeMillis(it.third)
MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first), MediaPlayerData.addMediaPlayer(
panel, clock, isSsReactivated = false) it.first,
it.second.copy(notificationKey = it.first),
panel,
clock,
isSsReactivated = false
)
} }
for ((index, key) in MediaPlayerData.playerKeys().withIndex()) { for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
@@ -205,8 +300,13 @@ class MediaCarouselControllerTest : SysuiTestCase() {
testPlayerOrdering() testPlayerOrdering()
// If smartspace is prioritized // If smartspace is prioritized
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, MediaPlayerData.addMediaRecommendation(
true, clock) SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
panel,
true,
clock
)
// Then it should be shown immediately after any actively playing controls // Then it should be shown immediately after any actively playing controls
assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
@@ -218,9 +318,9 @@ class MediaCarouselControllerTest : SysuiTestCase() {
// If smartspace is prioritized // If smartspace is prioritized
listener.value.onSmartspaceMediaDataLoaded( listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY, SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
true true
) )
// Then it should be shown immediately after any actively playing controls // Then it should be shown immediately after any actively playing controls
@@ -233,8 +333,13 @@ class MediaCarouselControllerTest : SysuiTestCase() {
testPlayerOrdering() testPlayerOrdering()
// If smartspace is not prioritized // If smartspace is not prioritized
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel, MediaPlayerData.addMediaRecommendation(
false, clock) SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
panel,
false,
clock
)
// Then it should be shown at the end of the carousel's active entries // Then it should be shown at the end of the carousel's active entries
val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1 val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
@@ -245,25 +350,36 @@ class MediaCarouselControllerTest : SysuiTestCase() {
fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() { fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
testPlayerOrdering() testPlayerOrdering()
// playing paused player // playing paused player
listener.value.onMediaDataLoaded("paused local", listener.value.onMediaDataLoaded(
"paused local", "paused local",
DATA.copy(active = true, isPlaying = true, "paused local",
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)) DATA.copy(
listener.value.onMediaDataLoaded("playing local", active = true,
"playing local", isPlaying = true,
DATA.copy(active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true) resumption = false
)
)
listener.value.onMediaDataLoaded(
"playing local",
"playing local",
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
)
) )
assertEquals( assertEquals(
MediaPlayerData.getMediaPlayerIndex("paused local"), MediaPlayerData.getMediaPlayerIndex("paused local"),
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
) )
// paused player order should stays the same in visibleMediaPLayer map. // paused player order should stays the same in visibleMediaPLayer map.
// paused player order should be first in mediaPlayer map. // paused player order should be first in mediaPlayer map.
assertEquals( assertEquals(
MediaPlayerData.visiblePlayerKeys().elementAt(3), MediaPlayerData.visiblePlayerKeys().elementAt(3),
MediaPlayerData.playerKeys().elementAt(0) MediaPlayerData.playerKeys().elementAt(0)
) )
} }
@Test @Test
@@ -285,7 +401,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged( mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_QS, MediaHierarchyManager.LOCATION_QS,
mediaHostState, mediaHostState,
animate = false) animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS) verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
} }
@@ -294,7 +411,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged( mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_QQS,
mediaHostState, mediaHostState,
animate = false) animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
} }
@@ -303,7 +421,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged( mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_LOCKSCREEN, MediaHierarchyManager.LOCATION_LOCKSCREEN,
mediaHostState, mediaHostState,
animate = false) animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
} }
@@ -312,7 +431,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged( mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_DREAM_OVERLAY, MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
mediaHostState, mediaHostState,
animate = false) animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
} }
@@ -321,10 +441,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
val packageName = "smartspace package" val packageName = "smartspace package"
val instanceId = InstanceId.fakeInstanceId(123) val instanceId = InstanceId.fakeInstanceId(123)
val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( val smartspaceData =
packageName = packageName, EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId)
instanceId = instanceId
)
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock) MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
mediaCarouselController.removePlayer(SMARTSPACE_KEY) mediaCarouselController.removePlayer(SMARTSPACE_KEY)
@@ -333,64 +451,99 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test @Test
fun testMediaLoaded_ScrollToActivePlayer() { fun testMediaLoaded_ScrollToActivePlayer() {
listener.value.onMediaDataLoaded("playing local", listener.value.onMediaDataLoaded(
null, "playing local",
DATA.copy(active = true, isPlaying = true, null,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
)
)
listener.value.onMediaDataLoaded(
"paused local",
null,
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
)
) )
listener.value.onMediaDataLoaded("paused local",
null,
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
// adding a media recommendation card. // adding a media recommendation card.
listener.value.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, listener.value.onSmartspaceMediaDataLoaded(
false) SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
false
)
mediaCarouselController.shouldScrollToKey = true mediaCarouselController.shouldScrollToKey = true
// switching between media players. // switching between media players.
listener.value.onMediaDataLoaded("playing local", listener.value.onMediaDataLoaded(
"playing local", "playing local",
DATA.copy(active = true, isPlaying = false, "playing local",
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true) DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
)
)
listener.value.onMediaDataLoaded(
"paused local",
"paused local",
DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
)
) )
listener.value.onMediaDataLoaded("paused local",
"paused local",
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
assertEquals( assertEquals(
MediaPlayerData.getMediaPlayerIndex("paused local"), MediaPlayerData.getMediaPlayerIndex("paused local"),
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
) )
} }
@Test @Test
fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
listener.value.onSmartspaceMediaDataLoaded( listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY, SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
false false
) )
listener.value.onMediaDataLoaded("playing local", listener.value.onMediaDataLoaded(
null, "playing local",
DATA.copy(active = true, isPlaying = true, null,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false) DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
)
) )
var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
assertEquals( assertEquals(
playerIndex, playerIndex,
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
) )
assertEquals(playerIndex, 0) assertEquals(playerIndex, 0)
// Replaying the same media player one more time. // Replaying the same media player one more time.
// And check that the card stays in its position. // And check that the card stays in its position.
mediaCarouselController.shouldScrollToKey = true mediaCarouselController.shouldScrollToKey = true
listener.value.onMediaDataLoaded("playing local", listener.value.onMediaDataLoaded(
null, "playing local",
DATA.copy(active = true, isPlaying = true, null,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false, DATA.copy(
packageName = "PACKAGE_NAME") active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false,
packageName = "PACKAGE_NAME"
)
) )
playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local") playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
assertEquals(playerIndex, 0) assertEquals(playerIndex, 0)
@@ -423,37 +576,43 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test @Test
fun testGetCurrentVisibleMediaContentIntent() { fun testGetCurrentVisibleMediaContentIntent() {
val clickIntent1 = mock(PendingIntent::class.java) val clickIntent1 = mock(PendingIntent::class.java)
val player1 = Triple("player1", val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L)
DATA.copy(clickIntent = clickIntent1),
1000L)
clock.setCurrentTimeMillis(player1.third) clock.setCurrentTimeMillis(player1.third)
MediaPlayerData.addMediaPlayer(player1.first, MediaPlayerData.addMediaPlayer(
player1.second.copy(notificationKey = player1.first), player1.first,
panel, clock, isSsReactivated = false) player1.second.copy(notificationKey = player1.first),
panel,
clock,
isSsReactivated = false
)
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
val clickIntent2 = mock(PendingIntent::class.java) val clickIntent2 = mock(PendingIntent::class.java)
val player2 = Triple("player2", val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L)
DATA.copy(clickIntent = clickIntent2),
2000L)
clock.setCurrentTimeMillis(player2.third) clock.setCurrentTimeMillis(player2.third)
MediaPlayerData.addMediaPlayer(player2.first, MediaPlayerData.addMediaPlayer(
player2.second.copy(notificationKey = player2.first), player2.first,
panel, clock, isSsReactivated = false) player2.second.copy(notificationKey = player2.first),
panel,
clock,
isSsReactivated = false
)
// mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
// added to the front because it was active more recently. // added to the front because it was active more recently.
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
val clickIntent3 = mock(PendingIntent::class.java) val clickIntent3 = mock(PendingIntent::class.java)
val player3 = Triple("player3", val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L)
DATA.copy(clickIntent = clickIntent3),
500L)
clock.setCurrentTimeMillis(player3.third) clock.setCurrentTimeMillis(player3.third)
MediaPlayerData.addMediaPlayer(player3.first, MediaPlayerData.addMediaPlayer(
player3.second.copy(notificationKey = player3.first), player3.first,
panel, clock, isSsReactivated = false) player3.second.copy(notificationKey = player3.first),
panel,
clock,
isSsReactivated = false
)
// mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
// added to the end because it was active less recently. // added to the end because it was active less recently.
@@ -463,10 +622,14 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test @Test
fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
val delta = 0.0001F val delta = 0.0001F
val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation( val paginationSquishMiddle =
(PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION) TRANSFORM_BEZIER.getInterpolation(
val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation( (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
(PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION) )
val paginationSquishEnd =
TRANSFORM_BEZIER.getInterpolation(
(PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION
)
whenever(mediaHostStatesManager.mediaHostStates) whenever(mediaHostStatesManager.mediaHostStates)
.thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
whenever(mediaHostState.visible).thenReturn(true) whenever(mediaHostState.visible).thenReturn(true)

View File

@@ -60,7 +60,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.bluetooth.BroadcastDialogController import com.android.systemui.bluetooth.BroadcastDialogController
import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.models.player.MediaAction import com.android.systemui.media.controls.models.player.MediaAction
import com.android.systemui.media.controls.models.player.MediaButton import com.android.systemui.media.controls.models.player.MediaButton
@@ -68,9 +67,10 @@ import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.media.controls.models.player.SeekBarViewModel import com.android.systemui.media.controls.models.player.SeekBarViewModel
import com.android.systemui.media.controls.models.recommendation.KEY_SMARTSPACE_APP_NAME
import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.KEY_SMARTSPACE_APP_NAME import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.media.dialog.MediaOutputDialogFactory
@@ -177,8 +177,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var session: MediaSession private lateinit var session: MediaSession
private lateinit var device: MediaDeviceData private lateinit var device: MediaDeviceData
private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, private val disabledDevice =
showBroadcastButton = false) MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false)
private lateinit var mediaData: MediaData private lateinit var mediaData: MediaData
private val clock = FakeSystemClock() private val clock = FakeSystemClock()
@Mock private lateinit var logger: MediaUiEventLogger @Mock private lateinit var logger: MediaUiEventLogger
@@ -225,24 +225,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE) whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
context.setMockPackageManager(packageManager) context.setMockPackageManager(packageManager)
player = object : MediaControlPanel( player =
context, object :
bgExecutor, MediaControlPanel(
mainExecutor, context,
activityStarter, bgExecutor,
broadcastSender, mainExecutor,
mediaViewController, activityStarter,
seekBarViewModel, broadcastSender,
Lazy { mediaDataManager }, mediaViewController,
mediaOutputDialogFactory, seekBarViewModel,
mediaCarouselController, Lazy { mediaDataManager },
falsingManager, mediaOutputDialogFactory,
clock, mediaCarouselController,
logger, falsingManager,
keyguardStateController, clock,
activityIntentHelper, logger,
lockscreenUserManager, keyguardStateController,
broadcastDialogController) { activityIntentHelper,
lockscreenUserManager,
broadcastDialogController
) {
override fun loadAnimator( override fun loadAnimator(
animId: Int, animId: Int,
otionInterpolator: Interpolator, otionInterpolator: Interpolator,
@@ -263,18 +266,20 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Set valid recommendation data // Set valid recommendation data
val extras = Bundle() val extras = Bundle()
extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME)
val intent = Intent().apply { val intent =
putExtras(extras) Intent().apply {
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) putExtras(extras)
} setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
whenever(smartspaceAction.intent).thenReturn(intent) whenever(smartspaceAction.intent).thenReturn(intent)
whenever(smartspaceAction.extras).thenReturn(extras) whenever(smartspaceAction.extras).thenReturn(extras)
smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( smartspaceData =
packageName = PACKAGE, EMPTY_SMARTSPACE_MEDIA_DATA.copy(
instanceId = instanceId, packageName = PACKAGE,
recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), instanceId = instanceId,
cardAction = smartspaceAction recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
) cardAction = smartspaceAction
)
} }
private fun initGutsViewHolderMocks() { private fun initGutsViewHolderMocks() {
@@ -292,36 +297,39 @@ public class MediaControlPanelTest : SysuiTestCase() {
} }
private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) { private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
device = MediaDeviceData(true, null, name, null, device =
showBroadcastButton = shouldShowBroadcastButton) MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton)
// Create media session // Create media session
val metadataBuilder = MediaMetadata.Builder().apply { val metadataBuilder =
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) MediaMetadata.Builder().apply {
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
} putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
val playbackBuilder = PlaybackState.Builder().apply { }
setState(PlaybackState.STATE_PAUSED, 6000L, 1f) val playbackBuilder =
setActions(PlaybackState.ACTION_PLAY) PlaybackState.Builder().apply {
} setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
session = MediaSession(context, SESSION_KEY).apply { setActions(PlaybackState.ACTION_PLAY)
setMetadata(metadataBuilder.build()) }
setPlaybackState(playbackBuilder.build()) session =
} MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
session.setActive(true) session.setActive(true)
mediaData = MediaTestUtils.emptyMediaData.copy( mediaData =
MediaTestUtils.emptyMediaData.copy(
artist = ARTIST, artist = ARTIST,
song = TITLE, song = TITLE,
packageName = PACKAGE, packageName = PACKAGE,
token = session.sessionToken, token = session.sessionToken,
device = device, device = device,
instanceId = instanceId) instanceId = instanceId
)
} }
/** /** Initialize elements in media view holder */
* Initialize elements in media view holder
*/
private fun initMediaViewHolderMocks() { private fun initMediaViewHolderMocks() {
whenever(seekBarViewModel.progress).thenReturn(seekBarData) whenever(seekBarViewModel.progress).thenReturn(seekBarData)
@@ -362,7 +370,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
action1.id, action1.id,
action2.id, action2.id,
action3.id, action3.id,
action4.id) action4.id
)
} }
whenever(viewHolder.player).thenReturn(view) whenever(viewHolder.player).thenReturn(view)
@@ -407,9 +416,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
} }
/** /** Initialize elements for the recommendation view holder */
* Initialize elements for the recommendation view holder
*/
private fun initRecommendationViewHolderMocks() { private fun initRecommendationViewHolderMocks() {
recTitle1 = TextView(context) recTitle1 = TextView(context)
recTitle2 = TextView(context) recTitle2 = TextView(context)
@@ -432,9 +439,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
.thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
whenever(recommendationViewHolder.mediaTitles) whenever(recommendationViewHolder.mediaTitles)
.thenReturn(listOf(recTitle1, recTitle2, recTitle3)) .thenReturn(listOf(recTitle1, recTitle2, recTitle3))
whenever(recommendationViewHolder.mediaSubtitles).thenReturn( whenever(recommendationViewHolder.mediaSubtitles)
listOf(recSubtitle1, recSubtitle2, recSubtitle3) .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
)
whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder) whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
@@ -466,12 +472,13 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindSemanticActions() { fun bindSemanticActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background) val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
val semanticActions = MediaButton( val semanticActions =
playOrPause = MediaAction(icon, Runnable {}, "play", bg), MediaButton(
nextOrCustom = MediaAction(icon, Runnable {}, "next", bg), playOrPause = MediaAction(icon, Runnable {}, "play", bg),
custom0 = MediaAction(icon, null, "custom 0", bg), nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
custom1 = MediaAction(icon, null, "custom 1", bg) custom0 = MediaAction(icon, null, "custom 0", bg),
) custom1 = MediaAction(icon, null, "custom 1", bg)
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE) player.bindPlayer(state, PACKAGE)
@@ -514,15 +521,16 @@ public class MediaControlPanelTest : SysuiTestCase() {
val bg = context.getDrawable(R.drawable.qs_media_round_button_background) val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
// Setup button state: no prev or next button and their slots reserved // Setup button state: no prev or next button and their slots reserved
val semanticActions = MediaButton( val semanticActions =
playOrPause = MediaAction(icon, Runnable {}, "play", bg), MediaButton(
nextOrCustom = null, playOrPause = MediaAction(icon, Runnable {}, "play", bg),
prevOrCustom = null, nextOrCustom = null,
custom0 = MediaAction(icon, null, "custom 0", bg), prevOrCustom = null,
custom1 = MediaAction(icon, null, "custom 1", bg), custom0 = MediaAction(icon, null, "custom 0", bg),
false, custom1 = MediaAction(icon, null, "custom 1", bg),
true false,
) true
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -543,15 +551,16 @@ public class MediaControlPanelTest : SysuiTestCase() {
val bg = context.getDrawable(R.drawable.qs_media_round_button_background) val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
// Setup button state: no prev or next button and their slots reserved // Setup button state: no prev or next button and their slots reserved
val semanticActions = MediaButton( val semanticActions =
playOrPause = MediaAction(icon, Runnable {}, "play", bg), MediaButton(
nextOrCustom = null, playOrPause = MediaAction(icon, Runnable {}, "play", bg),
prevOrCustom = null, nextOrCustom = null,
custom0 = MediaAction(icon, null, "custom 0", bg), prevOrCustom = null,
custom1 = MediaAction(icon, null, "custom 1", bg), custom0 = MediaAction(icon, null, "custom 0", bg),
true, custom1 = MediaAction(icon, null, "custom 1", bg),
false true,
) false
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -659,10 +668,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
useRealConstraintSets() useRealConstraintSets()
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
playOrPause = MediaAction(icon, Runnable {}, "play", null), MediaButton(
nextOrCustom = MediaAction(icon, Runnable {}, "next", null) playOrPause = MediaAction(icon, Runnable {}, "play", null),
) nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -732,9 +742,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
useRealConstraintSets() useRealConstraintSets()
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
nextOrCustom = MediaAction(icon, Runnable {}, "next", null) MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null))
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -749,10 +758,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun bind_notScrubbing_scrubbingViewsGone() { fun bind_notScrubbing_scrubbingViewsGone() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
prevOrCustom = MediaAction(icon, {}, "prev", null), MediaButton(
nextOrCustom = MediaAction(icon, {}, "next", null) prevOrCustom = MediaAction(icon, {}, "prev", null),
) nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -783,10 +793,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
prevOrCustom = null, MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null))
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE) player.bindPlayer(state, PACKAGE)
@@ -803,10 +811,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
prevOrCustom = MediaAction(icon, {}, "prev", null), MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null)
nextOrCustom = null
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE) player.bindPlayer(state, PACKAGE)
@@ -823,10 +829,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
prevOrCustom = MediaAction(icon, {}, "prev", null), MediaButton(
nextOrCustom = MediaAction(icon, {}, "next", null) prevOrCustom = MediaAction(icon, {}, "prev", null),
) nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE) player.bindPlayer(state, PACKAGE)
@@ -845,10 +852,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() { fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton( val semanticActions =
prevOrCustom = MediaAction(icon, {}, "prev", null), MediaButton(
nextOrCustom = MediaAction(icon, {}, "next", null) prevOrCustom = MediaAction(icon, {}, "prev", null),
) nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions) val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -872,18 +880,20 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindNotificationActions() { fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play) val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background) val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
val actions = listOf( val actions =
MediaAction(icon, Runnable {}, "previous", bg), listOf(
MediaAction(icon, Runnable {}, "play", bg), MediaAction(icon, Runnable {}, "previous", bg),
MediaAction(icon, null, "next", bg), MediaAction(icon, Runnable {}, "play", bg),
MediaAction(icon, null, "custom 0", bg), MediaAction(icon, null, "next", bg),
MediaAction(icon, Runnable {}, "custom 1", bg) MediaAction(icon, null, "custom 0", bg),
) MediaAction(icon, Runnable {}, "custom 1", bg)
val state = mediaData.copy( )
actions = actions, val state =
actionsToShowInCompact = listOf(1, 2), mediaData.copy(
semanticActions = null actions = actions,
) actionsToShowInCompact = listOf(1, 2),
semanticActions = null
)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE) player.bindPlayer(state, PACKAGE)
@@ -931,15 +941,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
val icon = context.getDrawable(R.drawable.ic_media_play) val icon = context.getDrawable(R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.ic_media_play_container) val bg = context.getDrawable(R.drawable.ic_media_play_container)
val semanticActions0 = MediaButton( val semanticActions0 =
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null) MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
) val semanticActions1 =
val semanticActions1 = MediaButton( MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null))
playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null) val semanticActions2 =
) MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null))
val semanticActions2 = MediaButton(
playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)
)
val state0 = mediaData.copy(semanticActions = semanticActions0) val state0 = mediaData.copy(semanticActions = semanticActions0)
val state1 = mediaData.copy(semanticActions = semanticActions1) val state1 = mediaData.copy(semanticActions = semanticActions1)
val state2 = mediaData.copy(semanticActions = semanticActions2) val state2 = mediaData.copy(semanticActions = semanticActions2)
@@ -1102,11 +1109,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
val mockAvd0 = mock(AnimatedVectorDrawable::class.java) val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
whenever(mockAvd0.mutate()).thenReturn(mockAvd0) whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
val semanticActions0 = MediaButton( val semanticActions0 =
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null) MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
) val state =
val state = mediaData.copy(resumption = true, semanticActions = semanticActions0, mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false)
isPlaying = false)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE) player.bindPlayer(state, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(APP_NAME) assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
@@ -1445,9 +1451,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionPlayPauseClick_isLogged() { fun actionPlayPauseClick_isLogged() {
val semanticActions = MediaButton( val semanticActions =
playOrPause = MediaAction(null, Runnable {}, "play", null) MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null))
)
val data = mediaData.copy(semanticActions = semanticActions) val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1459,9 +1464,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionPrevClick_isLogged() { fun actionPrevClick_isLogged() {
val semanticActions = MediaButton( val semanticActions =
prevOrCustom = MediaAction(null, Runnable {}, "previous", null) MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null))
)
val data = mediaData.copy(semanticActions = semanticActions) val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1473,9 +1477,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionNextClick_isLogged() { fun actionNextClick_isLogged() {
val semanticActions = MediaButton( val semanticActions =
nextOrCustom = MediaAction(null, Runnable {}, "next", null) MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null))
)
val data = mediaData.copy(semanticActions = semanticActions) val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1487,9 +1490,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionCustom0Click_isLogged() { fun actionCustom0Click_isLogged() {
val semanticActions = MediaButton( val semanticActions =
custom0 = MediaAction(null, Runnable {}, "custom 0", null) MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null))
)
val data = mediaData.copy(semanticActions = semanticActions) val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1501,9 +1503,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionCustom1Click_isLogged() { fun actionCustom1Click_isLogged() {
val semanticActions = MediaButton( val semanticActions =
custom1 = MediaAction(null, Runnable {}, "custom 1", null) MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null))
)
val data = mediaData.copy(semanticActions = semanticActions) val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1515,13 +1516,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionCustom2Click_isLogged() { fun actionCustom2Click_isLogged() {
val actions = listOf( val actions =
MediaAction(null, Runnable {}, "action 0", null), listOf(
MediaAction(null, Runnable {}, "action 1", null), MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 2", null), MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 3", null), MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 4", null) MediaAction(null, Runnable {}, "action 3", null),
) MediaAction(null, Runnable {}, "action 4", null)
)
val data = mediaData.copy(actions = actions) val data = mediaData.copy(actions = actions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1533,13 +1535,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionCustom3Click_isLogged() { fun actionCustom3Click_isLogged() {
val actions = listOf( val actions =
MediaAction(null, Runnable {}, "action 0", null), listOf(
MediaAction(null, Runnable {}, "action 1", null), MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 2", null), MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 3", null), MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 4", null) MediaAction(null, Runnable {}, "action 3", null),
) MediaAction(null, Runnable {}, "action 4", null)
)
val data = mediaData.copy(actions = actions) val data = mediaData.copy(actions = actions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1551,13 +1554,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test @Test
fun actionCustom4Click_isLogged() { fun actionCustom4Click_isLogged() {
val actions = listOf( val actions =
MediaAction(null, Runnable {}, "action 0", null), listOf(
MediaAction(null, Runnable {}, "action 1", null), MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 2", null), MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 3", null), MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 4", null) MediaAction(null, Runnable {}, "action 3", null),
) MediaAction(null, Runnable {}, "action 4", null)
)
val data = mediaData.copy(actions = actions) val data = mediaData.copy(actions = actions)
player.attachPlayer(viewHolder) player.attachPlayer(viewHolder)
@@ -1621,8 +1625,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
// THEN it shows without dismissing keyguard first // THEN it shows without dismissing keyguard first
captor.value.onClick(viewHolder.player) captor.value.onClick(viewHolder.player)
verify(activityStarter).startActivity(eq(clickIntent), eq(true), verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true))
nullable(), eq(true))
} }
@Test @Test
@@ -1710,20 +1713,22 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_listHasTooFewRecs_notDisplayed() { fun bindRecommendation_listHasTooFewRecs_notDisplayed() {
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "title1") recommendations =
.setSubtitle("subtitle1") listOf(
.setIcon(icon) SmartspaceAction.Builder("id1", "title1")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle1")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id2", "title2") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle2") .build(),
.setIcon(icon) SmartspaceAction.Builder("id2", "title2")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle2")
.build(), .setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
@@ -1735,30 +1740,32 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() { fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() {
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "title1") recommendations =
.setSubtitle("subtitle1") listOf(
.setIcon(icon) SmartspaceAction.Builder("id1", "title1")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle1")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id2", "title2") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle2") .build(),
.setIcon(icon) SmartspaceAction.Builder("id2", "title2")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle2")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id2", "empty icon 1") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle2") .build(),
.setIcon(null) SmartspaceAction.Builder("id2", "empty icon 1")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle2")
.build(), .setIcon(null)
SmartspaceAction.Builder("id2", "empty icon 2") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle2") .build(),
.setIcon(null) SmartspaceAction.Builder("id2", "empty icon 2")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle2")
.build(), .setIcon(null)
.setExtras(Bundle.EMPTY)
.build(),
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
@@ -1778,25 +1785,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
val subtitle3 = "Subtitle3" val subtitle3 = "Subtitle3"
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", title1) recommendations =
.setSubtitle(subtitle1) listOf(
.setIcon(icon) SmartspaceAction.Builder("id1", title1)
.setExtras(Bundle.EMPTY) .setSubtitle(subtitle1)
.build(), .setIcon(icon)
SmartspaceAction.Builder("id2", title2) .setExtras(Bundle.EMPTY)
.setSubtitle(subtitle2) .build(),
.setIcon(icon) SmartspaceAction.Builder("id2", title2)
.setExtras(Bundle.EMPTY) .setSubtitle(subtitle2)
.build(), .setIcon(icon)
SmartspaceAction.Builder("id3", title3) .setExtras(Bundle.EMPTY)
.setSubtitle(subtitle3) .build(),
.setIcon(icon) SmartspaceAction.Builder("id3", title3)
.setExtras(Bundle.EMPTY) .setSubtitle(subtitle3)
.build() .setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
assertThat(recTitle1.text).isEqualTo(title1) assertThat(recTitle1.text).isEqualTo(title1)
@@ -1811,15 +1820,17 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_noTitle_subtitleNotShown() { fun bindRecommendation_noTitle_subtitleNotShown() {
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "") recommendations =
.setSubtitle("fake subtitle") listOf(
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) SmartspaceAction.Builder("id1", "")
.setExtras(Bundle.EMPTY) .setSubtitle("fake subtitle")
.build() .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
assertThat(recSubtitle1.text).isEqualTo("") assertThat(recSubtitle1.text).isEqualTo("")
@@ -1831,25 +1842,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "") recommendations =
.setSubtitle("fake subtitle") listOf(
.setIcon(icon) SmartspaceAction.Builder("id1", "")
.setExtras(Bundle.EMPTY) .setSubtitle("fake subtitle")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id2", "title2") .setExtras(Bundle.EMPTY)
.setSubtitle("fake subtitle") .build(),
.setIcon(icon) SmartspaceAction.Builder("id2", "title2")
.setExtras(Bundle.EMPTY) .setSubtitle("fake subtitle")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id3", "") .setExtras(Bundle.EMPTY)
.setSubtitle("fake subtitle") .build(),
.setIcon(icon) SmartspaceAction.Builder("id3", "")
.setExtras(Bundle.EMPTY) .setSubtitle("fake subtitle")
.build() .setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE) assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
@@ -1863,25 +1876,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata) val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "") recommendations =
.setSubtitle("") listOf(
.setIcon(icon) SmartspaceAction.Builder("id1", "")
.setExtras(Bundle.EMPTY) .setSubtitle("")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id2", "title2") .setExtras(Bundle.EMPTY)
.setSubtitle("") .build(),
.setIcon(icon) SmartspaceAction.Builder("id2", "title2")
.setExtras(Bundle.EMPTY) .setSubtitle("")
.build(), .setIcon(icon)
SmartspaceAction.Builder("id3", "title3") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle3") .build(),
.setIcon(icon) SmartspaceAction.Builder("id3", "title3")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle3")
.build() .setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE) assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
@@ -1893,25 +1908,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() { fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() {
useRealConstraintSets() useRealConstraintSets()
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "title1") recommendations =
.setSubtitle("") listOf(
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) SmartspaceAction.Builder("id1", "title1")
.setExtras(Bundle.EMPTY) .setSubtitle("")
.build(), .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
SmartspaceAction.Builder("id2", "title2") .setExtras(Bundle.EMPTY)
.setSubtitle("") .build(),
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) SmartspaceAction.Builder("id2", "title2")
.setExtras(Bundle.EMPTY) .setSubtitle("")
.build(), .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
SmartspaceAction.Builder("id3", "title3") .setExtras(Bundle.EMPTY)
.setSubtitle("") .build(),
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) SmartspaceAction.Builder("id3", "title3")
.setExtras(Bundle.EMPTY) .setSubtitle("")
.build() .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
@@ -1924,25 +1941,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() { fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() {
useRealConstraintSets() useRealConstraintSets()
player.attachRecommendation(recommendationViewHolder) player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy( val data =
recommendations = listOf( smartspaceData.copy(
SmartspaceAction.Builder("id1", "") recommendations =
.setSubtitle("subtitle1") listOf(
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)) SmartspaceAction.Builder("id1", "")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle1")
.build(), .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
SmartspaceAction.Builder("id2", "") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle2") .build(),
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) SmartspaceAction.Builder("id2", "")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle2")
.build(), .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
SmartspaceAction.Builder("id3", "") .setExtras(Bundle.EMPTY)
.setSubtitle("subtitle3") .build(),
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata)) SmartspaceAction.Builder("id3", "")
.setExtras(Bundle.EMPTY) .setSubtitle("subtitle3")
.build() .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
) )
)
player.bindRecommendation(data) player.bindRecommendation(data)
@@ -1955,20 +1974,23 @@ public class MediaControlPanelTest : SysuiTestCase() {
} }
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener = private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) } withArgCaptor {
verify(seekBarViewModel).setScrubbingChangeListener(capture())
}
private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor {
withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) } verify(seekBarViewModel).setEnabledChangeListener(capture())
}
/** /**
* Update our test to use real ConstraintSets instead of mocks. * Update our test to use real ConstraintSets instead of mocks.
* *
* Some item visibilities, such as the seekbar visibility, are dependent on other action's * Some item visibilities, such as the seekbar visibility, are dependent on other action's
* visibilities. If we use mocks for the ConstraintSets, then action visibility changes are * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just
* just thrown away instead of being saved for reference later. This method sets us up to use * thrown away instead of being saved for reference later. This method sets us up to use
* ConstraintSets so that we do save visibility changes. * ConstraintSets so that we do save visibility changes.
* *
* TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests? * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
*/ */
private fun useRealConstraintSets() { private fun useRealConstraintSets() {
expandedSet = ConstraintSet() expandedSet = ConstraintSet()

View File

@@ -84,10 +84,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)> private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)>
@Captor @Captor
private lateinit var dreamOverlayCallback: private lateinit var dreamOverlayCallback:
ArgumentCaptor<(DreamOverlayStateController.Callback)> ArgumentCaptor<(DreamOverlayStateController.Callback)>
@JvmField @JvmField @Rule val mockito = MockitoJUnit.rule()
@Rule
val mockito = MockitoJUnit.rule()
private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var mediaHierarchyManager: MediaHierarchyManager
private lateinit var mediaFrame: ViewGroup private lateinit var mediaFrame: ViewGroup
private val configurationController = FakeConfigurationController() private val configurationController = FakeConfigurationController()
@@ -98,13 +96,15 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
@Before @Before
fun setup() { fun setup() {
context.getOrCreateTestableResources().addOverride( context
R.bool.config_use_split_notification_shade, false) .getOrCreateTestableResources()
.addOverride(R.bool.config_use_split_notification_shade, false)
mediaFrame = FrameLayout(context) mediaFrame = FrameLayout(context)
testableLooper = TestableLooper.get(this) testableLooper = TestableLooper.get(this)
fakeHandler = FakeHandler(testableLooper.looper) fakeHandler = FakeHandler(testableLooper.looper)
whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
mediaHierarchyManager = MediaHierarchyManager( mediaHierarchyManager =
MediaHierarchyManager(
context, context,
statusBarStateController, statusBarStateController,
keyguardStateController, keyguardStateController,
@@ -116,7 +116,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
wakefulnessLifecycle, wakefulnessLifecycle,
notifPanelEvents, notifPanelEvents,
settings, settings,
fakeHandler,) fakeHandler,
)
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
verify(statusBarStateController).addCallback(statusBarCallback.capture()) verify(statusBarStateController).addCallback(statusBarCallback.capture())
verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture()) verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture())
@@ -125,7 +126,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
whenever(mediaCarouselController.mediaCarouselScrollHandler) whenever(mediaCarouselController.mediaCarouselScrollHandler)
.thenReturn(mediaCarouselScrollHandler) .thenReturn(mediaCarouselScrollHandler)
val observer = wakefullnessObserver.value val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer) assertNotNull("lifecycle observer wasn't registered", observer)
observer.onFinishedWakingUp() observer.onFinishedWakingUp()
@@ -151,30 +152,53 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
fun testBlockedWhenScreenTurningOff() { fun testBlockedWhenScreenTurningOff() {
// Let's set it onto QS: // Let's set it onto QS:
mediaHierarchyManager.qsExpansion = 1.0f mediaHierarchyManager.qsExpansion = 1.0f
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController)
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) .onDesiredLocationChanged(
ArgumentMatchers.anyInt(),
any(MediaHostState::class.java),
anyBoolean(),
anyLong(),
anyLong()
)
val observer = wakefullnessObserver.value val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer) assertNotNull("lifecycle observer wasn't registered", observer)
observer.onStartedGoingToSleep() observer.onStartedGoingToSleep()
clearInvocations(mediaCarouselController) clearInvocations(mediaCarouselController)
mediaHierarchyManager.qsExpansion = 0.0f mediaHierarchyManager.qsExpansion = 0.0f
verify(mediaCarouselController, times(0)) verify(mediaCarouselController, times(0))
.onDesiredLocationChanged(ArgumentMatchers.anyInt(), .onDesiredLocationChanged(
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) ArgumentMatchers.anyInt(),
any(MediaHostState::class.java),
anyBoolean(),
anyLong(),
anyLong()
)
} }
@Test @Test
fun testAllowedWhenNotTurningOff() { fun testAllowedWhenNotTurningOff() {
// Let's set it onto QS: // Let's set it onto QS:
mediaHierarchyManager.qsExpansion = 1.0f mediaHierarchyManager.qsExpansion = 1.0f
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController)
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) .onDesiredLocationChanged(
ArgumentMatchers.anyInt(),
any(MediaHostState::class.java),
anyBoolean(),
anyLong(),
anyLong()
)
val observer = wakefullnessObserver.value val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer) assertNotNull("lifecycle observer wasn't registered", observer)
clearInvocations(mediaCarouselController) clearInvocations(mediaCarouselController)
mediaHierarchyManager.qsExpansion = 0.0f mediaHierarchyManager.qsExpansion = 0.0f
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController)
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) .onDesiredLocationChanged(
ArgumentMatchers.anyInt(),
any(MediaHostState::class.java),
anyBoolean(),
anyLong(),
anyLong()
)
} }
@Test @Test
@@ -183,22 +207,26 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
// Let's transition all the way to full shade // Let's transition all the way to full shade
mediaHierarchyManager.setTransitionToFullShadeAmount(100000f) mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
verify(mediaCarouselController).onDesiredLocationChanged( verify(mediaCarouselController)
eq(MediaHierarchyManager.LOCATION_QQS), .onDesiredLocationChanged(
any(MediaHostState::class.java), eq(MediaHierarchyManager.LOCATION_QQS),
eq(false), any(MediaHostState::class.java),
anyLong(), eq(false),
anyLong()) anyLong(),
anyLong()
)
clearInvocations(mediaCarouselController) clearInvocations(mediaCarouselController)
// Let's go back to the lock screen // Let's go back to the lock screen
mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f) mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
verify(mediaCarouselController).onDesiredLocationChanged( verify(mediaCarouselController)
eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), .onDesiredLocationChanged(
any(MediaHostState::class.java), eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
eq(false), any(MediaHostState::class.java),
anyLong(), eq(false),
anyLong()) anyLong(),
anyLong()
)
// Let's make sure alpha is set // Let's make sure alpha is set
mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f) mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
@@ -302,7 +330,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
val expectedTranslation = LOCKSCREEN_TOP - QS_TOP val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()) assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
.isEqualTo(expectedTranslation) .isEqualTo(expectedTranslation)
} }
@Test @Test
@@ -343,27 +371,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
fun testDream() { fun testDream() {
goToDream() goToDream()
setMediaDreamComplicationEnabled(true) setMediaDreamComplicationEnabled(true)
verify(mediaCarouselController).onDesiredLocationChanged( verify(mediaCarouselController)
.onDesiredLocationChanged(
eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY), eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY),
nullable(), nullable(),
eq(false), eq(false),
anyLong(), anyLong(),
anyLong()) anyLong()
)
clearInvocations(mediaCarouselController) clearInvocations(mediaCarouselController)
setMediaDreamComplicationEnabled(false) setMediaDreamComplicationEnabled(false)
verify(mediaCarouselController).onDesiredLocationChanged( verify(mediaCarouselController)
.onDesiredLocationChanged(
eq(MediaHierarchyManager.LOCATION_QQS), eq(MediaHierarchyManager.LOCATION_QQS),
any(MediaHostState::class.java), any(MediaHostState::class.java),
eq(false), eq(false),
anyLong(), anyLong(),
anyLong()) anyLong()
)
} }
private fun enableSplitShade() { private fun enableSplitShade() {
context.getOrCreateTestableResources().addOverride( context
R.bool.config_use_split_notification_shade, true .getOrCreateTestableResources()
) .addOverride(R.bool.config_use_split_notification_shade, true)
configurationController.notifyConfigurationChanged() configurationController.notifyConfigurationChanged()
} }

View File

@@ -35,13 +35,10 @@ import org.mockito.junit.MockitoJUnit
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
public class MediaPlayerDataTest : SysuiTestCase() { public class MediaPlayerDataTest : SysuiTestCase() {
@Mock @Mock private lateinit var playerIsPlaying: MediaControlPanel
private lateinit var playerIsPlaying: MediaControlPanel
private var systemClock: FakeSystemClock = FakeSystemClock() private var systemClock: FakeSystemClock = FakeSystemClock()
@JvmField @JvmField @Rule val mockito = MockitoJUnit.rule()
@Rule
val mockito = MockitoJUnit.rule()
companion object { companion object {
val LOCAL = MediaData.PLAYBACK_LOCAL val LOCAL = MediaData.PLAYBACK_LOCAL
@@ -63,10 +60,20 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val playerIsRemote = mock(MediaControlPanel::class.java) val playerIsRemote = mock(MediaControlPanel::class.java)
val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION) val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) "2",
MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock, dataIsRemote,
isSsReactivated = false) playerIsRemote,
systemClock,
isSsReactivated = false
)
MediaPlayerData.addMediaPlayer(
"1",
dataIsPlaying,
playerIsPlaying,
systemClock,
isSsReactivated = false
)
val players = MediaPlayerData.players() val players = MediaPlayerData.players()
assertThat(players).hasSize(2) assertThat(players).hasSize(2)
@@ -81,22 +88,42 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val playerIsPlaying2 = mock(MediaControlPanel::class.java) val playerIsPlaying2 = mock(MediaControlPanel::class.java)
var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION) var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) "1",
dataIsPlaying1,
playerIsPlaying1,
systemClock,
isSsReactivated = false
)
systemClock.advanceTime(1) systemClock.advanceTime(1)
MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) "2",
dataIsPlaying2,
playerIsPlaying2,
systemClock,
isSsReactivated = false
)
systemClock.advanceTime(1) systemClock.advanceTime(1)
dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION) dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION)
dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION) dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION)
MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) "1",
dataIsPlaying1,
playerIsPlaying1,
systemClock,
isSsReactivated = false
)
systemClock.advanceTime(1) systemClock.advanceTime(1)
MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) "2",
dataIsPlaying2,
playerIsPlaying2,
systemClock,
isSsReactivated = false
)
systemClock.advanceTime(1) systemClock.advanceTime(1)
val players = MediaPlayerData.players() val players = MediaPlayerData.players()
@@ -124,26 +151,60 @@ public class MediaPlayerDataTest : SysuiTestCase() {
val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION) val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
MediaPlayerData.addMediaPlayer( MediaPlayerData.addMediaPlayer(
"3", dataIsStoppedAndLocal, playerIsStoppedAndLocal, systemClock, "3",
isSsReactivated = false) dataIsStoppedAndLocal,
playerIsStoppedAndLocal,
systemClock,
isSsReactivated = false
)
MediaPlayerData.addMediaPlayer( MediaPlayerData.addMediaPlayer(
"5", dataIsStoppedAndRemote, playerIsStoppedAndRemote, systemClock, "5",
isSsReactivated = false) dataIsStoppedAndRemote,
MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume, systemClock, playerIsStoppedAndRemote,
isSsReactivated = false) systemClock,
MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock, isSsReactivated = false
isSsReactivated = false) )
MediaPlayerData.addMediaPlayer( MediaPlayerData.addMediaPlayer(
"2", dataIsPlayingAndRemote, playerIsPlayingAndRemote, systemClock, "4",
isSsReactivated = false) dataCanResume,
MediaPlayerData.addMediaPlayer("6", dataUndetermined, playerUndetermined, systemClock, playerCanResume,
isSsReactivated = false) systemClock,
isSsReactivated = false
)
MediaPlayerData.addMediaPlayer(
"1",
dataIsPlaying,
playerIsPlaying,
systemClock,
isSsReactivated = false
)
MediaPlayerData.addMediaPlayer(
"2",
dataIsPlayingAndRemote,
playerIsPlayingAndRemote,
systemClock,
isSsReactivated = false
)
MediaPlayerData.addMediaPlayer(
"6",
dataUndetermined,
playerUndetermined,
systemClock,
isSsReactivated = false
)
val players = MediaPlayerData.players() val players = MediaPlayerData.players()
assertThat(players).hasSize(6) assertThat(players).hasSize(6)
assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote, assertThat(players)
playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined, .containsExactly(
playerCanResume).inOrder() playerIsPlaying,
playerIsPlayingAndRemote,
playerIsStoppedAndRemote,
playerIsStoppedAndLocal,
playerUndetermined,
playerCanResume
)
.inOrder()
} }
@Test @Test
@@ -155,13 +216,23 @@ public class MediaPlayerDataTest : SysuiTestCase() {
assertThat(MediaPlayerData.players()).hasSize(0) assertThat(MediaPlayerData.players()).hasSize(0)
MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) keyA,
data,
playerIsPlaying,
systemClock,
isSsReactivated = false
)
systemClock.advanceTime(1) systemClock.advanceTime(1)
assertThat(MediaPlayerData.players()).hasSize(1) assertThat(MediaPlayerData.players()).hasSize(1)
MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock, MediaPlayerData.addMediaPlayer(
isSsReactivated = false) keyB,
data,
playerIsPlaying,
systemClock,
isSsReactivated = false
)
systemClock.advanceTime(1) systemClock.advanceTime(1)
assertThat(MediaPlayerData.players()).hasSize(2) assertThat(MediaPlayerData.players()).hasSize(2)
@@ -179,12 +250,13 @@ public class MediaPlayerDataTest : SysuiTestCase() {
isPlaying: Boolean?, isPlaying: Boolean?,
location: Int, location: Int,
resumption: Boolean resumption: Boolean
) = MediaTestUtils.emptyMediaData.copy( ) =
app = app, MediaTestUtils.emptyMediaData.copy(
packageName = "package: $app", app = app,
playbackLocation = location, packageName = "package: $app",
resumption = resumption, playbackLocation = location,
notificationKey = "key: $app", resumption = resumption,
isPlaying = isPlaying notificationKey = "key: $app",
) isPlaying = isPlaying
)
} }

View File

@@ -16,7 +16,6 @@
package com.android.systemui.media.controls.ui package com.android.systemui.media.controls.ui
import org.mockito.Mockito.`when` as whenever
import android.animation.Animator import android.animation.Animator
import android.test.suitebuilder.annotation.SmallTest import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
@@ -29,10 +28,11 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.times
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
@SmallTest @SmallTest
@@ -55,8 +55,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
handler = MetadataAnimationHandler(exitAnimator, enterAnimator) handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
} }
@After @After fun tearDown() {}
fun tearDown() {}
@Test @Test
fun firstBind_startsAnimationSet() { fun firstBind_startsAnimationSet() {

View File

@@ -50,12 +50,9 @@ class SquigglyProgressTest : SysuiTestCase() {
private val tint = Color.GREEN private val tint = Color.GREEN
lateinit var squigglyProgress: SquigglyProgress lateinit var squigglyProgress: SquigglyProgress
@Mock @Mock lateinit var canvas: Canvas
lateinit var canvas: Canvas @Captor lateinit var paintCaptor: ArgumentCaptor<Paint>
@Captor @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
lateinit var paintCaptor: ArgumentCaptor<Paint>
@JvmField @Rule
val mockitoRule = MockitoJUnit.rule()
@Before @Before
fun setup() { fun setup() {
@@ -126,7 +123,6 @@ class SquigglyProgressTest : SysuiTestCase() {
val (wavePaint, linePaint) = paintCaptor.getAllValues() val (wavePaint, linePaint) = paintCaptor.getAllValues()
assertThat(wavePaint.color).isEqualTo(tint) assertThat(wavePaint.color).isEqualTo(tint)
assertThat(linePaint.color).isEqualTo( assertThat(linePaint.color).isEqualTo(ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA))
ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA))
} }
} }