Format media control files am: 0d8858f204

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

Change-Id: Ic96d8269500159da4b2c9c835b223702c634931d
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Beth Thibodeau
2022-10-22 13:53:30 +00:00
committed by Automerger Merge Worker
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/LogcatEchoTrackerDebug.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/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/dialog/MediaOutputBroadcastDialogFactory.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/lifecycle/InstantTaskExecutorRule.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/nearby/NearbyMediaDevicesManagerTest.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 {
val ids = setOf(
R.id.remove_text,
R.id.cancel,
R.id.dismiss,
R.id.settings
)
val ids = setOf(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(
val userId: Int,
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?,
/**
* App icon shown on player.
*/
/** App icon shown on player. */
val appIcon: Icon?,
/**
* Artist name.
*/
/** Artist name. */
val artist: CharSequence?,
/**
* Song name.
*/
/** Song name. */
val song: CharSequence?,
/**
* Album artwork.
*/
/** Album artwork. */
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>,
/**
* 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>,
/**
* Semantic actions buttons, based on the PlaybackState of the media session.
* If present, these actions will be preferred in the UI over [actions]
* Semantic actions buttons, based on the PlaybackState of the media session. If present, these
* actions will be preferred in the UI over [actions]
*/
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,
/**
* Unique media session identifier.
*/
/** Unique media session identifier. */
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?,
/**
* 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?,
/**
* When active, a player will be displayed on keyguard and quick-quick settings.
* This is unrelated to the stream being playing or not, a player will not be active if
* timed out, or in resumption mode.
* When active, a player will be displayed on keyguard and quick-quick settings. This is
* unrelated to the stream being playing or not, a player will not be active if timed out, or in
* resumption mode.
*/
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?,
/**
* 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,
/**
* 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,
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,
/**
* 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,
/**
* Timestamp when this player was last active.
*/
/** Timestamp when this player was last active. */
var lastActive: Long = 0L,
/**
* Instance ID for logging purposes
*/
/** Instance ID for logging purposes */
val instanceId: InstanceId,
/**
* The UID of the app, used for logging
*/
/** The UID of the app, used for logging */
val appUid: Int
) {
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(
/**
* Play/pause button
*/
/** Play/pause button */
val playOrPause: MediaAction? = null,
/**
* Next button, or custom action
*/
/** Next button, or custom action */
val nextOrCustom: MediaAction? = null,
/**
* Previous button, or custom action
*/
/** Previous button, or custom action */
val prevOrCustom: MediaAction? = null,
/**
* First custom action space
*/
/** First custom action space */
val custom0: MediaAction? = null,
/**
* Second custom action space
*/
/** Second custom action space */
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,
/**
* 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
) {
fun getActionById(id: Int): MediaAction? {
@@ -201,7 +148,8 @@ data class MediaAction(
/** State of the media device. */
data class MediaDeviceData
@JvmOverloads constructor(
@JvmOverloads
constructor(
/** Whether or not to enable the chip */
val enabled: Boolean,
@@ -221,8 +169,8 @@ data class MediaDeviceData
val showBroadcastButton: Boolean
) {
/**
* Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon
* is ignored because it can change by reference frequently depending on the device type's
* Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is
* 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
*/
fun equalsWithoutIcon(other: MediaDeviceData?): Boolean {

View File

@@ -30,9 +30,7 @@ import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "MediaViewHolder"
/**
* Holder class for media player view
*/
/** Holder class for media player view */
class MediaViewHolder constructor(itemView: View) {
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
val scrubbingElapsedTimeView: TextView =
itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
val scrubbingTotalTimeView: TextView =
itemView.requireViewById(R.id.media_scrubbing_total_time)
val scrubbingTotalTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_total_time)
val gutsViewHolder = GutsViewHolder(itemView)
@@ -87,15 +84,7 @@ class MediaViewHolder constructor(itemView: View) {
}
fun getTransparentActionButtons(): List<ImageButton> {
return listOf(
actionNext,
actionPrev,
action0,
action1,
action2,
action3,
action4
)
return listOf(actionNext, actionPrev, action0, action1, action2, action3, action4)
}
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 parent Parent of inflated view.
*/
@JvmStatic fun create(
inflater: LayoutInflater,
parent: ViewGroup
): MediaViewHolder {
@JvmStatic
fun create(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder {
val mediaView = inflater.inflate(R.layout.media_session_view, parent, false)
mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// 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.app_name,
R.id.header_title,
@@ -143,27 +131,23 @@ class MediaViewHolder constructor(itemView: View) {
R.id.icon,
R.id.media_scrubbing_elapsed_time,
R.id.media_scrubbing_total_time
)
)
// Buttons used for notification-based actions
val genericButtonIds = setOf(
R.id.action0,
R.id.action1,
R.id.action2,
R.id.action3,
R.id.action4
)
val genericButtonIds =
setOf(R.id.action0, R.id.action1, R.id.action2, R.id.action3, R.id.action4)
val expandedBottomActionIds = setOf(
R.id.actionPrev,
R.id.actionNext,
R.id.action0,
R.id.action1,
R.id.action2,
R.id.action3,
R.id.action4,
R.id.media_scrubbing_elapsed_time,
R.id.media_scrubbing_total_time
)
val expandedBottomActionIds =
setOf(
R.id.actionPrev,
R.id.actionNext,
R.id.action0,
R.id.action1,
R.id.action2,
R.id.action3,
R.id.action4,
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.
*/
open class SeekBarObserver(
private val holder: MediaViewHolder
) : Observer<SeekBarViewModel.Progress> {
open class SeekBarObserver(private val holder: MediaViewHolder) :
Observer<SeekBarViewModel.Progress> {
companion object {
@JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750
@JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
}
val seekBarEnabledMaxHeight = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height)
val seekBarDisabledHeight = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height)
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)
val seekBarEnabledMaxHeight =
holder.seekBar.context.resources.getDimensionPixelSize(
R.dimen.qs_media_enabled_seekbar_height
)
val seekBarDisabledHeight =
holder.seekBar.context.resources.getDimensionPixelSize(
R.dimen.qs_media_disabled_seekbar_height
)
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
init {
val seekBarProgressWavelength = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength).toFloat()
val seekBarProgressAmplitude = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude).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 seekBarProgressWavelength =
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength)
.toFloat()
val seekBarProgressAmplitude =
holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude)
.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
progressDrawable?.let {
it.waveLength = seekBarProgressWavelength
@@ -98,16 +113,18 @@ open class SeekBarObserver(
}
holder.seekBar.setMax(data.duration)
val totalTimeString = DateUtils.formatElapsedTime(
data.duration / DateUtils.SECOND_IN_MILLIS)
val totalTimeString =
DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
holder.scrubbingTotalTimeView.text = totalTimeString
}
data.elapsedTime?.let {
if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) {
if (it <= RESET_ANIMATION_THRESHOLD_MS &&
holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS) {
if (
it <= RESET_ANIMATION_THRESHOLD_MS &&
holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS
) {
// This animation resets for every additional update to zero.
val animator = buildResetAnimator(it)
animator.start()
@@ -117,24 +134,29 @@ open class SeekBarObserver(
}
}
val elapsedTimeString = DateUtils.formatElapsedTime(
it / DateUtils.SECOND_IN_MILLIS)
val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
holder.scrubbingElapsedTimeView.text = elapsedTimeString
}
holder.seekBar.contentDescription = holder.seekBar.context.getString(
R.string.controls_media_seekbar_description,
elapsedTimeString,
totalTimeString
)
holder.seekBar.contentDescription =
holder.seekBar.context.getString(
R.string.controls_media_seekbar_description,
elapsedTimeString,
totalTimeString
)
}
}
@VisibleForTesting
open fun buildResetAnimator(targetTime: Int): Animator {
val animator = ObjectAnimator.ofInt(holder.seekBar, "progress",
holder.seekBar.progress, targetTime + RESET_ANIMATION_DURATION_MS)
val animator =
ObjectAnimator.ofInt(
holder.seekBar,
"progress",
holder.seekBar.progress,
targetTime + RESET_ANIMATION_DURATION_MS
)
animator.setAutoCancel(true)
animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
animator.interpolator = Interpolators.EMPHASIZED

View File

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

View File

@@ -35,26 +35,30 @@ class RecommendationViewHolder private constructor(itemView: View) {
// Recommendation screen
val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
val mediaCoverItems = listOf<ImageView>(
itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover2),
itemView.requireViewById(R.id.media_cover3)
)
val mediaCoverContainers = listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover1_container),
itemView.requireViewById(R.id.media_cover2_container),
itemView.requireViewById(R.id.media_cover3_container)
)
val mediaTitles: List<TextView> = listOf(
itemView.requireViewById(R.id.media_title1),
itemView.requireViewById(R.id.media_title2),
itemView.requireViewById(R.id.media_title3)
)
val mediaSubtitles: List<TextView> = listOf(
itemView.requireViewById(R.id.media_subtitle1),
itemView.requireViewById(R.id.media_subtitle2),
itemView.requireViewById(R.id.media_subtitle3)
)
val mediaCoverItems =
listOf<ImageView>(
itemView.requireViewById(R.id.media_cover1),
itemView.requireViewById(R.id.media_cover2),
itemView.requireViewById(R.id.media_cover3)
)
val mediaCoverContainers =
listOf<ViewGroup>(
itemView.requireViewById(R.id.media_cover1_container),
itemView.requireViewById(R.id.media_cover2_container),
itemView.requireViewById(R.id.media_cover3_container)
)
val mediaTitles: List<TextView> =
listOf(
itemView.requireViewById(R.id.media_title1),
itemView.requireViewById(R.id.media_title2),
itemView.requireViewById(R.id.media_title3)
)
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)
@@ -78,13 +82,14 @@ class RecommendationViewHolder private constructor(itemView: View) {
* @param inflater LayoutInflater to use to inflate the layout.
* @param parent Parent of inflated view.
*/
@JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup):
RecommendationViewHolder {
@JvmStatic
fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
val itemView =
inflater.inflate(
R.layout.media_smartspace_recommendations,
parent,
false /* attachToRoot */)
false /* attachToRoot */
)
// 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
// 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.
val controlsIds = setOf(
R.id.recommendation_card_icon,
R.id.media_cover1,
R.id.media_cover2,
R.id.media_cover3,
R.id.media_cover1_container,
R.id.media_cover2_container,
R.id.media_cover3_container,
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
R.id.media_subtitle1,
R.id.media_subtitle2,
R.id.media_subtitle3
)
val controlsIds =
setOf(
R.id.recommendation_card_icon,
R.id.media_cover1,
R.id.media_cover2,
R.id.media_cover3,
R.id.media_cover1_container,
R.id.media_cover2_container,
R.id.media_cover3_container,
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
R.id.media_subtitle1,
R.id.media_subtitle2,
R.id.media_subtitle3
)
val mediaTitlesAndSubtitlesIds = setOf(
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
R.id.media_subtitle1,
R.id.media_subtitle2,
R.id.media_subtitle3
)
val mediaTitlesAndSubtitlesIds =
setOf(
R.id.media_title1,
R.id.media_title2,
R.id.media_title3,
R.id.media_subtitle1,
R.id.media_subtitle2,
R.id.media_subtitle3
)
val mediaContainersIds = setOf(
R.id.media_cover1_container,
R.id.media_cover2_container,
R.id.media_cover3_container
)
val mediaContainersIds =
setOf(
R.id.media_cover1_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 com.android.internal.logging.InstanceId
@VisibleForTesting
const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
@VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
/** State of a Smartspace media recommendations view. */
data class SmartspaceMediaData(
/**
* Unique id of a Smartspace media target.
*/
/** Unique id of a Smartspace media target. */
val targetId: String,
/**
* Indicates if the status is active.
*/
/** Indicates if the status is active. */
val isActive: Boolean,
/**
* Package name of the media recommendations' provider-app.
*/
/** Package name of the media recommendations' provider-app. */
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?,
/**
* List of media recommendations.
*/
/** List of media recommendations. */
val recommendations: List<SmartspaceAction>,
/**
* Intent for the user's initiated dismissal.
*/
/** Intent for the user's initiated dismissal. */
val dismissIntent: Intent?,
/**
* The timestamp in milliseconds that headphone is connected.
*/
/** The timestamp in milliseconds that headphone is connected. */
val headphoneConnectionTimeMillis: Long,
/**
* Instance ID for [MediaUiEventLogger]
*/
/** Instance ID for [MediaUiEventLogger] */
val instanceId: InstanceId
) {
/**
* Indicates if all the data is valid.
*
* TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than
* ```
* [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 }
/** Returns the upstream app name if available. */
@@ -92,7 +75,8 @@ data class SmartspaceMediaData(
Log.w(
TAG,
"Package $packageName does not have a main launcher activity. " +
"Fallback to full app name")
"Fallback to full app name"
)
return try {
val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0)
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
import android.app.smartspace.SmartspaceTarget
@@ -23,7 +39,7 @@ class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin
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>) {
// Filter out non-media targets.
val mediaTargets = mutableListOf<SmartspaceTarget>()

View File

@@ -17,17 +17,15 @@
package com.android.systemui.media.controls.pipeline
import android.content.Context
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.InfoMediaManager
import com.android.settingslib.media.LocalMediaManager
import javax.inject.Inject
/**
* Factory to create [LocalMediaManager] objects.
*/
class LocalMediaManagerFactory @Inject constructor(
/** Factory to create [LocalMediaManager] objects. */
class LocalMediaManagerFactory
@Inject
constructor(
private val context: Context,
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 javax.inject.Inject
/**
* Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events.
*/
class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
MediaDeviceManager.Listener {
/** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */
class MediaDataCombineLatest @Inject constructor() :
MediaDataManager.Listener, MediaDeviceManager.Listener {
private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
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) }
}
override fun onMediaDeviceChanged(
key: String,
oldKey: String?,
data: MediaDeviceData?
) {
override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) {
if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
entries[key] = entries.remove(oldKey)?.first to data
update(key, oldKey)
@@ -86,9 +80,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.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)
private fun update(key: String, oldKey: String?) {
@@ -96,18 +88,14 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
if (entry != null && device != null) {
val data = entry.copy(device = device)
val listenersCopy = listeners.toSet()
listenersCopy.forEach {
it.onMediaDataLoaded(key, oldKey, data)
}
listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) }
}
}
private fun remove(key: String) {
entries.remove(key)?.let {
val listenersCopy = listeners.toSet()
listenersCopy.forEach {
it.onMediaDataRemoved(key)
}
listenersCopy.forEach { 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.BroadcastSender
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.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.CurrentUserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.time.SystemClock
@@ -37,7 +37,8 @@ import kotlin.collections.LinkedHashMap
private const val TAG = "MediaDataFilter"
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")
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.
*/
@VisibleForTesting
internal val SMARTSPACE_MAX_AGE = SystemProperties
.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
internal val SMARTSPACE_MAX_AGE =
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
@@ -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
* background users (e.g. timeouts).
*/
class MediaDataFilter @Inject constructor(
class MediaDataFilter
@Inject
constructor(
private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher,
private val broadcastSender: BroadcastSender,
@@ -79,12 +82,13 @@ class MediaDataFilter @Inject constructor(
private var reactivatedKey: String? = null
init {
userTracker = object : CurrentUserTracker(broadcastDispatcher) {
override fun onUserSwitched(newUserId: Int) {
// Post this so we can be sure lockscreenUserManager already got the broadcast
executor.execute { handleUserSwitched(newUserId) }
userTracker =
object : CurrentUserTracker(broadcastDispatcher) {
override fun onUserSwitched(newUserId: Int) {
// Post this so we can be sure lockscreenUserManager already got the broadcast
executor.execute { handleUserSwitched(newUserId) }
}
}
}
userTracker.startTracking()
}
@@ -111,9 +115,7 @@ class MediaDataFilter @Inject constructor(
userEntries.put(key, data)
// Notify listeners
listeners.forEach {
it.onMediaDataLoaded(key, oldKey, data)
}
listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
}
override fun onSmartspaceMediaDataLoaded(
@@ -131,14 +133,11 @@ class MediaDataFilter @Inject constructor(
smartspaceMediaData = data
// Before forwarding the smartspace target, first check if we have recently inactive media
val sorted = userEntries.toSortedMap(compareBy {
userEntries.get(it)?.lastActive ?: -1
})
val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
data.cardAction?.let {
val smartspaceMaxAgeSeconds =
it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
if (smartspaceMaxAgeSeconds > 0) {
smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
}
@@ -155,13 +154,21 @@ class MediaDataFilter @Inject constructor(
Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
reactivatedKey = lastActiveKey
val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName,
mediaData.instanceId)
logger.logRecommendationActivated(
mediaData.appUid,
mediaData.packageName,
mediaData.instanceId
)
listeners.forEach {
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData,
receivedSmartspaceCardLatency =
it.onMediaDataLoaded(
lastActiveKey,
lastActiveKey,
mediaData,
receivedSmartspaceCardLatency =
(systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
.toInt(), isSsReactivated = true)
.toInt(),
isSsReactivated = true
)
}
}
} else {
@@ -173,8 +180,10 @@ class MediaDataFilter @Inject constructor(
Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
return
}
logger.logRecommendationAdded(smartspaceMediaData.packageName,
smartspaceMediaData.instanceId)
logger.logRecommendationAdded(
smartspaceMediaData.packageName,
smartspaceMediaData.instanceId
)
listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
}
@@ -182,9 +191,7 @@ class MediaDataFilter @Inject constructor(
allEntries.remove(key)
userEntries.remove(key)?.let {
// Only notify listeners if something actually changed
listeners.forEach {
it.onMediaDataRemoved(key)
}
listeners.forEach { it.onMediaDataRemoved(key) }
}
}
@@ -197,16 +204,17 @@ class MediaDataFilter @Inject constructor(
// Notify listeners to update with actual active value
userEntries.get(lastActiveKey)?.let { mediaData ->
listeners.forEach {
it.onMediaDataLoaded(
lastActiveKey, lastActiveKey, mediaData, immediately)
it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
}
}
}
if (smartspaceMediaData.isActive) {
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId)
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId
)
}
listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
}
@@ -221,25 +229,19 @@ class MediaDataFilter @Inject constructor(
userEntries.clear()
keyCopy.forEach {
if (DEBUG) Log.d(TAG, "Removing $it after user change")
listenersCopy.forEach { listener ->
listener.onMediaDataRemoved(it)
}
listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
}
allEntries.forEach { (key, data) ->
if (lockscreenUserManager.isCurrentProfile(data.userId)) {
if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
userEntries.put(key, data)
listenersCopy.forEach { listener ->
listener.onMediaDataLoaded(key, null, data)
}
listenersCopy.forEach { listener -> 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() {
if (DEBUG) Log.d(TAG, "Media carousel swiped away")
val mediaKeys = userEntries.keys.toSet()
@@ -250,55 +252,52 @@ class MediaDataFilter @Inject constructor(
if (smartspaceMediaData.isActive) {
val dismissIntent = smartspaceMediaData.dismissIntent
if (dismissIntent == null) {
Log.w(TAG, "Cannot create dismiss action click action: " +
"extras missing dismiss_intent.")
} else if (dismissIntent.getComponent() != null &&
dismissIntent.getComponent().getClassName()
== EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) {
Log.w(
TAG,
"Cannot create dismiss action click action: " + "extras missing dismiss_intent."
)
} else if (
dismissIntent.getComponent() != null &&
dismissIntent.getComponent().getClassName() ==
EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
) {
// Dismiss the card Smartspace data through Smartspace trampoline activity.
context.startActivity(dismissIntent)
} else {
broadcastSender.sendBroadcast(dismissIntent)
}
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId)
mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId,
delay = 0L)
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId
)
mediaDataManager.dismissSmartspaceRecommendation(
smartspaceMediaData.targetId,
delay = 0L
)
}
}
/**
* Are there any active media entries, including the recommendation?
*/
fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } ||
/** Are there any active media entries, including the recommendation? */
fun hasActiveMediaOrRecommendation() =
userEntries.any { it.value.active } ||
(smartspaceMediaData.isActive &&
(smartspaceMediaData.isValid() || reactivatedKey != null))
/**
* Are there any media entries we should display?
*/
fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() ||
(smartspaceMediaData.isActive && smartspaceMediaData.isValid())
/** Are there any media entries we should display? */
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 }
/**
* 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()
/**
* Add a listener for filtered [MediaData] changes
*/
/** Add a listener for filtered [MediaData] changes */
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)
/**
@@ -318,8 +317,6 @@ class MediaDataFilter @Inject constructor(
val now = systemClock.elapsedRealtime()
val lastActiveKey = sortedEntries.lastKey() // most recently active
return sortedEntries.get(lastActiveKey)?.let {
now - it.lastActive
} ?: Long.MAX_VALUE
return sortedEntries.get(lastActiveKey)?.let { 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.Main
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.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.MediaMuteAwaitConnectionManagerFactory
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 DEBUG = true
/**
* Provides information about the route (ie. device) where playback is occurring.
*/
class MediaDeviceManager @Inject constructor(
/** Provides information about the route (ie. device) where playback is occurring. */
class MediaDeviceManager
@Inject
constructor(
private val context: Context,
private val controllerFactory: MediaControllerFactory,
private val localMediaManagerFactory: LocalMediaManagerFactory,
@@ -74,14 +74,10 @@ class MediaDeviceManager @Inject constructor(
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)
/**
* 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)
override fun onMediaDataLoaded(
@@ -105,19 +101,11 @@ class MediaDeviceManager @Inject constructor(
processDevice(key, oldKey, data.device)
return
}
val controller = data.token?.let {
controllerFactory.create(it)
}
val controller = data.token?.let { controllerFactory.create(it) }
val localMediaManager = localMediaManagerFactory.create(data.packageName)
val muteAwaitConnectionManager =
muteAwaitConnectionManagerFactory.create(localMediaManager)
entry = Entry(
key,
oldKey,
controller,
localMediaManager,
muteAwaitConnectionManager
)
muteAwaitConnectionManagerFactory.create(localMediaManager)
entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
entries[key] = entry
entry.start()
}
@@ -126,11 +114,7 @@ class MediaDeviceManager @Inject constructor(
override fun onMediaDataRemoved(key: String) {
val token = entries.remove(key)
token?.stop()
token?.let {
listeners.forEach {
it.onKeyRemoved(key)
}
}
token?.let { listeners.forEach { it.onKeyRemoved(key) } }
}
override fun dump(pw: PrintWriter, args: Array<String>) {
@@ -145,9 +129,7 @@ class MediaDeviceManager @Inject constructor(
@MainThread
private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
listeners.forEach {
it.onMediaDeviceChanged(key, oldKey, device)
}
listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
}
interface Listener {
@@ -163,8 +145,10 @@ class MediaDeviceManager @Inject constructor(
val controller: MediaController?,
val localMediaManager: LocalMediaManager,
val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
) : LocalMediaManager.DeviceCallback, MediaController.Callback(),
BluetoothLeBroadcast.Callback {
) :
LocalMediaManager.DeviceCallback,
MediaController.Callback(),
BluetoothLeBroadcast.Callback {
val token
get() = controller?.sessionToken
@@ -175,54 +159,52 @@ class MediaDeviceManager @Inject constructor(
val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
if (!started || !sameWithoutIcon) {
field = value
fgExecutor.execute {
processDevice(key, oldKey, value)
}
fgExecutor.execute { processDevice(key, oldKey, value) }
}
}
// 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.
private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
private var broadcastDescription: String? = null
private val configListener = object : ConfigurationController.ConfigurationListener {
override fun onLocaleListChanged() {
updateCurrent()
private val configListener =
object : ConfigurationController.ConfigurationListener {
override fun onLocaleListChanged() {
updateCurrent()
}
}
}
@AnyThread
fun start() = bgExecutor.execute {
if (!started) {
localMediaManager.registerCallback(this)
localMediaManager.startScan()
muteAwaitConnectionManager?.startListening()
playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
controller?.registerCallback(this)
updateCurrent()
started = true
configurationController.addCallback(configListener)
fun start() =
bgExecutor.execute {
if (!started) {
localMediaManager.registerCallback(this)
localMediaManager.startScan()
muteAwaitConnectionManager?.startListening()
playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
controller?.registerCallback(this)
updateCurrent()
started = true
configurationController.addCallback(configListener)
}
}
}
@AnyThread
fun stop() = bgExecutor.execute {
if (started) {
started = false
controller?.unregisterCallback(this)
localMediaManager.stopScan()
localMediaManager.unregisterCallback(this)
muteAwaitConnectionManager?.stopListening()
configurationController.removeCallback(configListener)
fun stop() =
bgExecutor.execute {
if (started) {
started = false
controller?.unregisterCallback(this)
localMediaManager.stopScan()
localMediaManager.unregisterCallback(this)
muteAwaitConnectionManager?.stopListening()
configurationController.removeCallback(configListener)
}
}
}
fun dump(pw: PrintWriter) {
val routingSession = controller?.let {
mr2manager.getRoutingSessionForMediaController(it)
}
val selectedRoutes = routingSession?.let {
mr2manager.getSelectedRoutes(it)
}
val routingSession =
controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
with(pw) {
println(" current device is ${current?.name}")
val type = controller?.playbackInfo?.playbackType
@@ -242,14 +224,11 @@ class MediaDeviceManager @Inject constructor(
updateCurrent()
}
override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute {
updateCurrent()
}
override fun onDeviceListUpdate(devices: List<MediaDevice>?) =
bgExecutor.execute { updateCurrent() }
override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
bgExecutor.execute {
updateCurrent()
}
bgExecutor.execute { updateCurrent() }
}
override fun onAboutToConnectDeviceAdded(
@@ -257,14 +236,17 @@ class MediaDeviceManager @Inject constructor(
deviceName: String,
deviceIcon: Drawable?
) {
aboutToConnectDeviceOverride = AboutToConnectDevice(
fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
backupMediaDeviceData = MediaDeviceData(
/* enabled */ enabled = true,
/* icon */ deviceIcon,
/* name */ deviceName,
/* showBroadcastButton */ showBroadcastButton = false)
)
aboutToConnectDeviceOverride =
AboutToConnectDevice(
fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
backupMediaDeviceData =
MediaDeviceData(
/* enabled */ enabled = true,
/* icon */ deviceIcon,
/* name */ deviceName,
/* showBroadcastButton */ showBroadcastButton = false
)
)
updateCurrent()
}
@@ -291,8 +273,11 @@ class MediaDeviceManager @Inject constructor(
metadata: BluetoothLeBroadcastMetadata
) {
if (DEBUG) {
Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
"metadata = $metadata")
Log.d(
TAG,
"onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
"metadata = $metadata"
)
}
updateCurrent()
}
@@ -319,8 +304,10 @@ class MediaDeviceManager @Inject constructor(
override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
if (DEBUG) {
Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " +
"broadcastId = $broadcastId")
Log.d(
TAG,
"onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId"
)
}
}
@@ -331,34 +318,45 @@ class MediaDeviceManager @Inject constructor(
@WorkerThread
private fun updateCurrent() {
if (isLeAudioBroadcastEnabled()) {
current = MediaDeviceData(
current =
MediaDeviceData(
/* enabled */ true,
/* icon */ context.getDrawable(R.drawable.settings_input_antenna),
/* name */ broadcastDescription,
/* intent */ null,
/* showBroadcastButton */ showBroadcastButton = true)
/* showBroadcastButton */ showBroadcastButton = true
)
} else {
val aboutToConnect = aboutToConnectDeviceOverride
if (aboutToConnect != null &&
if (
aboutToConnect != null &&
aboutToConnect.fullMediaDevice == null &&
aboutToConnect.backupMediaDeviceData != null) {
aboutToConnect.backupMediaDeviceData != null
) {
// Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
current = aboutToConnect.backupMediaDeviceData
return
}
val device = aboutToConnect?.fullMediaDevice
?: localMediaManager.currentConnectedDevice
val device =
aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
// 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 name = if (controller == null || route != null) {
route?.name?.toString() ?: device?.name
} else {
null
}
current = MediaDeviceData(enabled, device?.iconWithoutBackground, name,
id = device?.id, showBroadcastButton = false)
val name =
if (controller == null || route != null) {
route?.name?.toString() ?: device?.name
} else {
null
}
current =
MediaDeviceData(
enabled,
device?.iconWithoutBackground,
name,
id = device?.id,
showBroadcastButton = false
)
}
}
@@ -388,13 +386,16 @@ class MediaDeviceManager @Inject constructor(
// unexpected result.
// Check the current media app's name is the same with current broadcast app's name
// or not.
var mediaApp = MediaDataUtils.getAppLabel(
context, localMediaManager.packageName,
context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name))
var mediaApp =
MediaDataUtils.getAppLabel(
context,
localMediaManager.packageName,
context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
)
var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
if (isCurrentBroadcastedApp) {
broadcastDescription = context.getString(
R.string.broadcasting_description_is_broadcasting)
broadcastDescription =
context.getString(R.string.broadcasting_description_is_broadcasting)
} else {
broadcastDescription = currentBroadcastedApp
}
@@ -407,9 +408,9 @@ class MediaDeviceManager @Inject constructor(
* [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
*
* @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
* 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(
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
* achieve this, update events for the local session need to be filtered.
*/
class MediaSessionBasedFilter @Inject constructor(
class MediaSessionBasedFilter
@Inject
constructor(
context: Context,
private val sessionManager: MediaSessionManager,
@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
// filter loaded events for local sessions.
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
// 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.
private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener {
override fun onActiveSessionsChanged(controllers: List<MediaController>) {
handleControllersChanged(controllers)
private val sessionListener =
object : MediaSessionManager.OnActiveSessionsChangedListener {
override fun onActiveSessionsChanged(controllers: List<MediaController>) {
handleControllersChanged(controllers)
}
}
}
init {
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)
/**
* Remove a listener that was registered with addListener
*/
/** Remove a listener that was registered with addListener */
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
/**
@@ -102,31 +101,32 @@ class MediaSessionBasedFilter @Inject constructor(
isSsReactivated: Boolean
) {
backgroundExecutor.execute {
data.token?.let {
tokensWithNotifications.add(it)
}
data.token?.let { tokensWithNotifications.add(it) }
val isMigration = oldKey != null && key != oldKey
if (isMigration) {
keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
}
if (data.token != null) {
keyedTokens.get(key)?.let {
tokens ->
tokens.add(data.token)
} ?: run {
val tokens = mutableSetOf(data.token)
keyedTokens.put(key, tokens)
}
keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
?: run {
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
// PLAYBACK_TYPE_REMOTE.
val remoteControllers = packageControllers.get(data.packageName)?.filter {
it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
}
val remoteControllers =
packageControllers.get(data.packageName)?.filter {
it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
}
// Limiting search to only apps with a single remote session.
val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
if (isMigration || remote == null || remote.sessionToken == data.token ||
!tokensWithNotifications.contains(remote.sessionToken)) {
if (
isMigration ||
remote == null ||
remote.sessionToken == data.token ||
!tokensWithNotifications.contains(remote.sessionToken)
) {
// Not filtering in this case. Passing the event along to listeners.
dispatchMediaDataLoaded(key, oldKey, data, immediately)
} else {
@@ -148,9 +148,7 @@ class MediaSessionBasedFilter @Inject constructor(
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
backgroundExecutor.execute {
dispatchSmartspaceMediaDataLoaded(key, data)
}
backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
}
override fun onMediaDataRemoved(key: String) {
@@ -162,9 +160,7 @@ class MediaSessionBasedFilter @Inject constructor(
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
backgroundExecutor.execute {
dispatchSmartspaceMediaDataRemoved(key, immediately)
}
backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) }
}
private fun dispatchMediaDataLoaded(
@@ -179,9 +175,7 @@ class MediaSessionBasedFilter @Inject constructor(
}
private fun dispatchMediaDataRemoved(key: String) {
foregroundExecutor.execute {
listeners.toSet().forEach { it.onMediaDataRemoved(key) }
}
foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } }
}
private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
@@ -198,15 +192,12 @@ class MediaSessionBasedFilter @Inject constructor(
private fun handleControllersChanged(controllers: List<MediaController>) {
packageControllers.clear()
controllers.forEach {
controller ->
packageControllers.get(controller.packageName)?.let {
tokens ->
tokens.add(controller)
} ?: run {
val tokens = mutableListOf(controller)
packageControllers.put(controller.packageName, tokens)
}
controllers.forEach { controller ->
packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) }
?: run {
val tokens = mutableListOf(controller)
packageControllers.put(controller.packageName, tokens)
}
}
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.systemui.dagger.SysUISingleton
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.util.MediaControllerFactory
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -33,18 +33,18 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
@VisibleForTesting
val PAUSED_MEDIA_TIMEOUT = SystemProperties
.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
val PAUSED_MEDIA_TIMEOUT =
SystemProperties.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
@VisibleForTesting
val RESUME_MEDIA_TIMEOUT = SystemProperties
.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
val RESUME_MEDIA_TIMEOUT =
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
class MediaTimeoutListener @Inject constructor(
class MediaTimeoutListener
@Inject
constructor(
private val mediaControllerFactory: MediaControllerFactory,
@Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger,
@@ -58,7 +58,9 @@ class MediaTimeoutListener @Inject constructor(
* Callback representing that a media object is now expired:
* @param key Media control unique identifier
* @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media,
* ```
* or {@code RESUME_MEDIA_TIMEOUT} for resume media
* ```
*/
lateinit var timeoutCallback: (String, Boolean) -> Unit
@@ -70,21 +72,25 @@ class MediaTimeoutListener @Inject constructor(
lateinit var stateCallback: (String, PlaybackState) -> Unit
init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
override fun onDozingChanged(isDozing: Boolean) {
if (!isDozing) {
// Check whether any timeouts should have expired
mediaListeners.forEach { (key, listener) ->
if (listener.cancellation != null &&
listener.expiration <= systemClock.elapsedRealtime()) {
// We dozed too long - timeout now, and cancel the pending one
listener.expireMediaTimeout(key, "timeout happened while dozing")
listener.doTimeout()
statusBarStateController.addCallback(
object : StatusBarStateController.StateListener {
override fun onDozingChanged(isDozing: Boolean) {
if (!isDozing) {
// Check whether any timeouts should have expired
mediaListeners.forEach { (key, listener) ->
if (
listener.cancellation != null &&
listener.expiration <= systemClock.elapsedRealtime()
) {
// We dozed too long - timeout now, and cancel the pending one
listener.expireMediaTimeout(key, "timeout happened while dozing")
listener.doTimeout()
}
}
}
}
}
})
)
}
override fun onMediaDataLoaded(
@@ -147,10 +153,8 @@ class MediaTimeoutListener @Inject constructor(
return mediaListeners[key]?.timedOut ?: false
}
private inner class PlaybackStateListener(
var key: String,
data: MediaData
) : MediaController.Callback() {
private inner class PlaybackStateListener(var key: String, data: MediaData) :
MediaController.Callback() {
var timedOut = false
var lastState: PlaybackState? = null
@@ -164,11 +168,12 @@ class MediaTimeoutListener @Inject constructor(
mediaController?.unregisterCallback(this)
field = value
val token = field.token
mediaController = if (token != null) {
mediaControllerFactory.create(token)
} else {
null
}
mediaController =
if (token != null) {
mediaControllerFactory.create(token)
} else {
null
}
mediaController?.registerCallback(this)
// Let's register the cancellations, but not dispatch events now.
// Timeouts didn't happen yet and reentrant events are troublesome.
@@ -214,7 +219,8 @@ class MediaTimeoutListener @Inject constructor(
logger.logPlaybackState(key, state)
val playingStateSame = (state?.state?.isPlaying() == isPlaying())
val actionsSame = (lastState?.actions == state?.actions) &&
val actionsSame =
(lastState?.actions == state?.actions) &&
areCustomActionListsEqual(lastState?.customActions, state?.customActions)
val resumptionChanged = resumption != mediaData.resumption
@@ -239,15 +245,14 @@ class MediaTimeoutListener @Inject constructor(
return
}
expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption")
val timeout = if (mediaData.resumption) {
RESUME_MEDIA_TIMEOUT
} else {
PAUSED_MEDIA_TIMEOUT
}
val timeout =
if (mediaData.resumption) {
RESUME_MEDIA_TIMEOUT
} else {
PAUSED_MEDIA_TIMEOUT
}
expiration = systemClock.elapsedRealtime() + timeout
cancellation = mainExecutor.executeDelayed({
doTimeout()
}, timeout)
cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
} else {
expireMediaTimeout(key, "playback started - $state, $key")
timedOut = false
@@ -303,9 +308,11 @@ class MediaTimeoutListener @Inject constructor(
firstAction: PlaybackState.CustomAction,
secondAction: PlaybackState.CustomAction
): Boolean {
if (firstAction.action != secondAction.action ||
if (
firstAction.action != secondAction.action ||
firstAction.name != secondAction.name ||
firstAction.icon != secondAction.icon) {
firstAction.icon != secondAction.icon
) {
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.LogLevel
import javax.inject.Inject
private const val TAG = "MediaTimeout"
/**
* A buffered log for [MediaTimeoutListener] events
*/
/** A buffered log for [MediaTimeoutListener] events */
@SysUISingleton
class MediaTimeoutLogger @Inject constructor(
@MediaTimeoutListenerLog private val buffer: LogBuffer
) {
fun logReuseListener(key: String) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
},
{
"reuse listener: $str1"
}
)
class MediaTimeoutLogger
@Inject
constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) {
fun logReuseListener(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "reuse listener: $str1" })
fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = oldKey
str2 = newKey
bool1 = hadListener
},
{
"migrate from $str1 to $str2, had listener? $bool1"
}
)
fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) =
buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = oldKey
str2 = newKey
bool1 = hadListener
},
{ "migrate from $str1 to $str2, had listener? $bool1" }
)
fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
bool1 = wasPlaying
},
{
"updating $str1, was playing? $bool1"
}
)
fun logUpdateListener(key: String, wasPlaying: Boolean) =
buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
bool1 = wasPlaying
},
{ "updating $str1, was playing? $bool1" }
)
fun logDelayedUpdate(key: String) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
},
{
"deliver delayed playback state for $str1"
}
)
fun logDelayedUpdate(key: String) =
buffer.log(
TAG,
LogLevel.DEBUG,
{ str1 = key },
{ "deliver delayed playback state for $str1" }
)
fun logSessionDestroyed(key: String) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
},
{
"session destroyed $str1"
}
)
fun logSessionDestroyed(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "session destroyed $str1" })
fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log(
TAG,
LogLevel.VERBOSE,
{
str1 = key
str2 = state?.toString()
},
{
"state update: key=$str1 state=$str2"
}
)
fun logStateCallback(key: String) = buffer.log(
fun logPlaybackState(key: String, state: PlaybackState?) =
buffer.log(
TAG,
LogLevel.VERBOSE,
{
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(
TAG,
LogLevel.DEBUG,
{
str1 = key
bool1 = playing
bool2 = resumption
},
{
"schedule timeout $str1, playing=$bool1 resumption=$bool2"
}
)
fun logCancelIgnored(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "cancellation already exists for $str1" })
fun logCancelIgnored(key: String) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
},
{
"cancellation already exists for $str1"
}
)
fun logTimeout(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "execute timeout for $str1" })
fun logTimeout(key: String) = buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
},
{
"execute timeout for $str1"
}
)
fun logTimeoutCancelled(key: String, reason: String) = buffer.log(
TAG,
LogLevel.VERBOSE,
{
str1 = key
str2 = reason
},
{
"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_"
@SysUISingleton
class MediaResumeListener @Inject constructor(
class MediaResumeListener
@Inject
constructor(
private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor,
@@ -62,7 +64,7 @@ class MediaResumeListener @Inject constructor(
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
ConcurrentLinkedQueue()
ConcurrentLinkedQueue()
private lateinit var mediaDataManager: MediaDataManager
@@ -75,40 +77,49 @@ class MediaResumeListener @Inject constructor(
private var currentUserId: Int = context.userId
@VisibleForTesting
val userChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_USER_UNLOCKED == intent.action) {
loadMediaResumptionControls()
} else if (Intent.ACTION_USER_SWITCHED == intent.action) {
currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
loadSavedComponents()
val userChangeReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_USER_UNLOCKED == intent.action) {
loadMediaResumptionControls()
} else if (Intent.ACTION_USER_SWITCHED == intent.action) {
currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
loadSavedComponents()
}
}
}
}
private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
override fun addTrack(
desc: MediaDescription,
component: ComponentName,
browser: ResumeMediaBrowser
) {
val token = browser.token
val appIntent = browser.appIntent
val pm = context.getPackageManager()
var appName: CharSequence = component.packageName
val resumeAction = getResumeAction(component)
try {
appName = pm.getApplicationLabel(
pm.getApplicationInfo(component.packageName, 0))
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Error getting package information", e)
private val mediaBrowserCallback =
object : ResumeMediaBrowser.Callback() {
override fun addTrack(
desc: MediaDescription,
component: ComponentName,
browser: ResumeMediaBrowser
) {
val token = browser.token
val appIntent = browser.appIntent
val pm = context.getPackageManager()
var appName: CharSequence = component.packageName
val resumeAction = getResumeAction(component)
try {
appName =
pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0))
} 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 {
if (useMediaResumption) {
@@ -116,8 +127,12 @@ class MediaResumeListener @Inject constructor(
val unlockFilter = IntentFilter()
unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
unlockFilter.addAction(Intent.ACTION_USER_SWITCHED)
broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null,
UserHandle.ALL)
broadcastDispatcher.registerReceiver(
userChangeReceiver,
unlockFilter,
null,
UserHandle.ALL
)
loadSavedComponents()
}
}
@@ -126,12 +141,15 @@ class MediaResumeListener @Inject constructor(
mediaDataManager = manager
// Add listener for resumption setting changes
tunerService.addTunable(object : TunerService.Tunable {
override fun onTuningChanged(key: String?, newValue: String?) {
useMediaResumption = Utils.useMediaResumption(context)
mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
}
}, Settings.Secure.MEDIA_CONTROLS_RESUME)
tunerService.addTunable(
object : TunerService.Tunable {
override fun onTuningChanged(key: String?, newValue: String?) {
useMediaResumption = Utils.useMediaResumption(context)
mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
}
},
Settings.Secure.MEDIA_CONTROLS_RESUME
)
}
private fun loadSavedComponents() {
@@ -139,8 +157,10 @@ class MediaResumeListener @Inject constructor(
resumeComponents.clear()
val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
?.dropLastWhile { it.isEmpty() }
val components =
listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile {
it.isEmpty()
}
var needsUpdate = false
components?.forEach {
val info = it.split("/")
@@ -148,17 +168,18 @@ class MediaResumeListener @Inject constructor(
val className = info[1]
val component = ComponentName(packageName, className)
val lastPlayed = if (info.size == 3) {
try {
info[2].toLong()
} catch (e: NumberFormatException) {
val lastPlayed =
if (info.size == 3) {
try {
info[2].toLong()
} catch (e: NumberFormatException) {
needsUpdate = true
systemClock.currentTimeMillis()
}
} else {
needsUpdate = true
systemClock.currentTimeMillis()
}
} else {
needsUpdate = true
systemClock.currentTimeMillis()
}
resumeComponents.add(component to lastPlayed)
}
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() {
if (!useMediaResumption) {
return
@@ -207,9 +226,7 @@ class MediaResumeListener @Inject constructor(
val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
val inf = resumeInfo?.filter {
it.serviceInfo.packageName == data.packageName
}
val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
if (inf != null && inf.size > 0) {
backgroundExecutor.execute {
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")
// Set null action to prevent additional attempts to connect
mediaDataManager.setResumeAction(key, null)
mediaBrowser = mediaBrowserFactory.create(
mediaBrowser =
mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
Log.d(TAG, "Connected to $componentName")
@@ -253,7 +271,8 @@ class MediaResumeListener @Inject constructor(
mediaBrowser = null
}
},
componentName)
componentName
)
mediaBrowser?.testConnection()
}
@@ -288,9 +307,7 @@ class MediaResumeListener @Inject constructor(
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 {
return Runnable {
mediaBrowser = mediaBrowserFactory.create(null, componentName)
@@ -299,8 +316,6 @@ class MediaResumeListener @Inject constructor(
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
println("resumeComponents: $resumeComponents")
}
pw.apply { println("resumeComponents: $resumeComponents") }
}
}
}

View File

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

View File

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

View File

@@ -39,12 +39,11 @@ interface ColorTransition {
}
/**
* A [ColorTransition] that animates between two specific colors.
* It uses a ValueAnimator to execute the animation and interpolate between the source color and
* the target color.
* A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute
* the animation and interpolate between the source color and the target color.
*
* Selection of the target color from the scheme, and application of the interpolated color
* are delegated to callbacks.
* Selection of the target color from the scheme, and application of the interpolated color are
* delegated to callbacks.
*/
open class AnimatingColorTransition(
private val defaultColor: Int,
@@ -59,9 +58,8 @@ open class AnimatingColorTransition(
var targetColor: Int = defaultColor
override fun onAnimationUpdate(animation: ValueAnimator) {
currentColor = argbEvaluator.evaluate(
animation.animatedFraction, sourceColor, targetColor
) as Int
currentColor =
argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int
applyColor(currentColor)
}
@@ -91,111 +89,126 @@ open class AnimatingColorTransition(
}
typealias AnimatingColorTransitionFactory =
(Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
(Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
/**
* ColorSchemeTransition constructs a ColorTransition for each color in the scheme
* that needs to be transitioned when changed. It also sets up the assignment functions for sending
* the sending the interpolated colors to the appropriate views.
* ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be
* transitioned when changed. It also sets up the assignment functions for sending the sending the
* interpolated colors to the appropriate views.
*/
class ColorSchemeTransition internal constructor(
class ColorSchemeTransition
internal constructor(
private val context: Context,
private val mediaViewHolder: MediaViewHolder,
animatingColorTransitionFactory: AnimatingColorTransitionFactory
) {
constructor(context: Context, mediaViewHolder: MediaViewHolder) :
this(context, mediaViewHolder, ::AnimatingColorTransition)
constructor(
context: Context,
mediaViewHolder: MediaViewHolder
) : this(context, mediaViewHolder, ::AnimatingColorTransition)
val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
val surfaceColor = animatingColorTransitionFactory(
bgColor,
::surfaceFromScheme
) { surfaceColor ->
val colorList = ColorStateList.valueOf(surfaceColor)
mediaViewHolder.seamlessIcon.imageTintList = colorList
mediaViewHolder.seamlessText.setTextColor(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 surfaceColor =
animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor ->
val colorList = ColorStateList.valueOf(surfaceColor)
mediaViewHolder.seamlessIcon.imageTintList = colorList
mediaViewHolder.seamlessText.setTextColor(surfaceColor)
mediaViewHolder.albumView.backgroundTintList = colorList
mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
}
}
val colorSeamless = animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimary),
{ 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 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
val accentPrimary =
animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimary),
::accentPrimaryFromScheme
) { accentPrimary ->
val accentColorList = ColorStateList.valueOf(accentPrimary)
mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
}
mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
}
val textPrimaryInverse = animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimaryInverse),
::textPrimaryInverseFromScheme
) { textPrimaryInverse ->
mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse)
}
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 textSecondary = animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorSecondary),
::textSecondaryFromScheme
) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) }
val colorSeamless =
animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimary),
{ 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(
loadDefaultColor(R.attr.textColorTertiary),
::textTertiaryFromScheme
) { textTertiary ->
mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary)
}
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 colorTransitions = arrayOf(
surfaceColor,
colorSeamless,
accentPrimary,
accentSecondary,
textPrimary,
textPrimaryInverse,
textSecondary,
textTertiary,
)
val textPrimaryInverse =
animatingColorTransitionFactory(
loadDefaultColor(R.attr.textColorPrimaryInverse),
::textPrimaryInverseFromScheme
) { textPrimaryInverse ->
mediaViewHolder.actionPlayPause.imageTintList =
ColorStateList.valueOf(textPrimaryInverse)
}
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 {
return Utils.getColorAttr(context, id).defaultColor

View File

@@ -42,22 +42,20 @@ import org.xmlpull.v1.XmlPullParser
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
class IlluminationDrawable : Drawable() {
private var themeAttrs: IntArray? = null
private var cornerRadiusOverride = -1f
var cornerRadius = 0f
get() {
return if (cornerRadiusOverride >= 0) {
cornerRadiusOverride
} else {
field
get() {
return if (cornerRadiusOverride >= 0) {
cornerRadiusOverride
} else {
field
}
}
}
private var highlightColor = Color.TRANSPARENT
private var tmpHsl = floatArrayOf(0f, 0f, 0f)
private var paint = Paint()
@@ -65,22 +63,27 @@ class IlluminationDrawable : Drawable() {
private val lightSources = arrayListOf<LightSourceDrawable>()
private var backgroundColor = Color.TRANSPARENT
set(value) {
if (value == field) {
return
set(value) {
if (value == field) {
return
}
field = value
animateBackground()
}
field = value
animateBackground()
}
private var backgroundAnimation: ValueAnimator? = null
/**
* Draw background and gradient.
*/
/** Draw background and gradient. */
override fun draw(canvas: Canvas) {
canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(),
cornerRadius, cornerRadius, paint)
canvas.drawRoundRect(
0f,
0f,
bounds.width().toFloat(),
bounds.height().toFloat(),
cornerRadius,
cornerRadius,
paint
)
}
override fun getOutline(outline: Outline) {
@@ -105,12 +108,11 @@ class IlluminationDrawable : Drawable() {
private fun updateStateFromTypedArray(a: TypedArray) {
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)) {
highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) /
100f
highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
}
}
@@ -163,34 +165,42 @@ class IlluminationDrawable : Drawable() {
private fun animateBackground() {
ColorUtils.colorToHSL(backgroundColor, tmpHsl)
val L = tmpHsl[2]
tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) {
L + highlight
} else {
L - highlight
}, 0f, 1f)
tmpHsl[2] =
MathUtils.constrain(
if (L < 1f - highlight) {
L + highlight
} else {
L - highlight
},
0f,
1f
)
val initialBackground = paint.color
val initialHighlight = highlightColor
val finalHighlight = ColorUtils.HSLToColor(tmpHsl)
backgroundAnimation?.cancel()
backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply {
duration = BACKGROUND_ANIM_DURATION
interpolator = Interpolators.FAST_OUT_LINEAR_IN
addUpdateListener {
val progress = it.animatedValue as Float
paint.color = blendARGB(initialBackground, backgroundColor, progress)
highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
lightSources.forEach { it.highlightColor = highlightColor }
invalidateSelf()
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
backgroundAnimation = null
backgroundAnimation =
ValueAnimator.ofFloat(0f, 1f).apply {
duration = BACKGROUND_ANIM_DURATION
interpolator = Interpolators.FAST_OUT_LINEAR_IN
addUpdateListener {
val progress = it.animatedValue as Float
paint.color = blendARGB(initialBackground, backgroundColor, progress)
highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
lightSources.forEach { it.highlightColor = highlightColor }
invalidateSelf()
}
})
start()
}
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
backgroundAnimation = null
}
}
)
start()
}
}
override fun setTintList(tint: ColorStateList?) {
@@ -215,4 +225,4 @@ class IlluminationDrawable : Drawable() {
fun setCornerRadiusOverride(cornerRadius: Float?) {
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
*/
@SysUISingleton
class KeyguardMediaController @Inject constructor(
class KeyguardMediaController
@Inject
constructor(
@param:Named(KEYGUARD) private val mediaHost: MediaHost,
private val bypassController: KeyguardBypassController,
private val statusBarStateController: SysuiStatusBarStateController,
@@ -56,34 +58,40 @@ class KeyguardMediaController @Inject constructor(
) {
init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
override fun onStateChanged(newState: Int) {
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
)
statusBarStateController.addCallback(
object : StatusBarStateController.StateListener {
override fun onStateChanged(newState: Int) {
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(
Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
settingsObserver,
UserHandle.USER_ALL)
Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
settingsObserver,
UserHandle.USER_ALL
)
// First let's set the desired state that we want for this host
mediaHost.expansion = MediaHostState.EXPANDED
@@ -110,27 +118,21 @@ class KeyguardMediaController @Inject constructor(
refreshMediaPosition()
}
/**
* Is the media player visible?
*/
/** Is the media player visible? */
var visible = false
private set
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
private set
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 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
@@ -146,9 +148,7 @@ class KeyguardMediaController @Inject constructor(
onMediaHostVisibilityChanged(mediaHost.visible)
}
/**
* Called whenever the media hosts visibility changes
*/
/** Called whenever the media hosts visibility changes */
private fun onMediaHostVisibilityChanged(visible: Boolean) {
refreshMediaPosition()
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) {
splitShadeContainer = container
reattachHostView()
@@ -183,9 +181,7 @@ class KeyguardMediaController @Inject constructor(
}
if (activeContainer?.childCount == 0) {
// Detach the hostView from its parent view if exists
mediaHost.hostView.parent?.let {
(it as? ViewGroup)?.removeView(mediaHost.hostView)
}
mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
activeContainer.addView(mediaHost.hostView)
}
}
@@ -193,7 +189,8 @@ class KeyguardMediaController @Inject constructor(
fun refreshMediaPosition() {
val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
// mediaHost.visible required for proper animations handling
visible = mediaHost.visible &&
visible =
mediaHost.visible &&
!bypassController.bypassEnabled &&
keyguardOrUserSwitcher &&
allowMediaPlayerOnLockScreen

View File

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

View File

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

View File

@@ -30,7 +30,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView
import java.util.Objects
import javax.inject.Inject
class MediaHost constructor(
class MediaHost
constructor(
private val state: MediaHostStateHolder,
private val mediaHierarchyManager: MediaHierarchyManager,
private val mediaDataManager: MediaDataManager,
@@ -45,14 +46,10 @@ class MediaHost constructor(
private var inited: Boolean = false
/**
* Are we listening to media data changes?
*/
/** Are we listening to media data changes? */
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()
get() {
hostView.getLocationOnScreen(tmpLocationOnScreen)
@@ -81,38 +78,39 @@ class MediaHost constructor(
*/
val currentClipping = Rect()
private val listener = object : MediaDataManager.Listener {
override fun onMediaDataLoaded(
key: String,
oldKey: String?,
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
isSsReactivated: Boolean
) {
if (immediately) {
private val listener =
object : MediaDataManager.Listener {
override fun onMediaDataLoaded(
key: String,
oldKey: String?,
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
isSsReactivated: Boolean
) {
if (immediately) {
updateViewVisibility()
}
}
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
updateViewVisibility()
}
}
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
updateViewVisibility()
}
override fun onMediaDataRemoved(key: String) {
updateViewVisibility()
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
if (immediately) {
override fun onMediaDataRemoved(key: String) {
updateViewVisibility()
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
if (immediately) {
updateViewVisibility()
}
}
}
}
fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
visibleChangedListeners.add(listener)
@@ -123,12 +121,14 @@ class MediaHost constructor(
}
/**
* Initialize this MediaObject and create a host view.
* All state should already be set on this host before calling this method in order to avoid
* unnecessary state changes which lead to remeasurings later on.
* Initialize this MediaObject and create a host view. All state should already be set on this
* host before calling this method in order to avoid unnecessary state changes which lead to
* remeasurings later on.
*
* @param location the location this host name has. Used to identify the host during
* ```
* transitions.
* ```
*/
fun init(@MediaLocation location: Int) {
if (inited) {
@@ -141,36 +141,42 @@ class MediaHost constructor(
// 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!
setListeningToMediaData(true)
hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
setListeningToMediaData(true)
updateViewVisibility()
}
hostView.addOnAttachStateChangeListener(
object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
setListeningToMediaData(true)
updateViewVisibility()
}
override fun onViewDetachedFromWindow(v: View?) {
setListeningToMediaData(false)
override fun onViewDetachedFromWindow(v: View?) {
setListeningToMediaData(false)
}
}
})
)
// Listen to measurement updates and update our state with it
hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
override fun onMeasure(input: MeasurementInput): MeasurementOutput {
// Modify the measurement to exactly match the dimensions
if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(input.widthMeasureSpec),
View.MeasureSpec.EXACTLY)
hostView.measurementManager =
object : UniqueObjectHostView.MeasurementManager {
override fun onMeasure(input: MeasurementInput): MeasurementOutput {
// Modify the measurement to exactly match the dimensions
if (
View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST
) {
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
state.changedListener = {
mediaHostStatesManager.updateHostState(location, state)
}
state.changedListener = { mediaHostStatesManager.updateHostState(location, state) }
updateViewVisibility()
}
@@ -191,17 +197,16 @@ class MediaHost constructor(
* the visibility has changed
*/
fun updateViewVisibility() {
state.visible = if (showsOnlyActiveMedia) {
mediaDataManager.hasActiveMediaOrRecommendation()
} else {
mediaDataManager.hasAnyMediaOrRecommendation()
}
state.visible =
if (showsOnlyActiveMedia) {
mediaDataManager.hasActiveMediaOrRecommendation()
} else {
mediaDataManager.hasAnyMediaOrRecommendation()
}
val newVisibility = if (visible) View.VISIBLE else View.GONE
if (newVisibility != hostView.visibility) {
hostView.visibility = newVisibility
visibleChangedListeners.forEach {
it.invoke(visible)
}
visibleChangedListeners.forEach { it.invoke(visible) }
}
}
@@ -269,14 +274,10 @@ class MediaHost constructor(
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
/**
* 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 {
val mediaHostState = MediaHostStateHolder()
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
* is hosted. The HostState notifies the media players of changes to their properties, who
* in turn will create view states from it.
* When adding a new property to this, make sure to update the listener and notify them
* about the changes.
* In case you need to have a different rendering based on the state, you can add a new
* constraintState to the [MediaViewController]. Otherwise, similar host states will resolve
* 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.
* A description of a media host state that describes the behavior whenever the media carousel is
* hosted. The HostState notifies the media players of changes to their properties, who in turn will
* create view states from it. When adding a new property to this, make sure to update the listener
* and notify them about the changes. In case you need to have a different rendering based on the
* state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host
* states will resolve 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 {
@@ -349,46 +348,36 @@ interface MediaHostState {
}
/**
* The last measurement input that this state was measured with. Infers width and height of
* the players.
* The last measurement input that this state was measured with. Infers width and height of the
* players.
*/
var measurementInput: MeasurementInput?
/**
* The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions),
* [EXPANDED] for fully expanded (up to 5 actions).
* The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED]
* for fully expanded (up to 5 actions).
*/
var expansion: Float
/**
* Fraction of the height animation.
*/
/** Fraction of the height animation. */
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
/**
* If the view should be VISIBLE or GONE.
*/
/** If the view should be VISIBLE or GONE. */
val visible: Boolean
/**
* Does this host need any falsing protection?
*/
/** Does this host need any falsing protection? */
var falsingProtectionNeeded: Boolean
/**
* 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
* are propagated
* visible. If modified, make sure to set this value again on the host to ensure the values are
* propagated
*/
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
}

View File

@@ -38,85 +38,76 @@ class MediaHostStatesManager @Inject constructor() {
*/
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()
/**
* Notify that a media state for a given location has changed. Should only be called from
* Media hosts themselves.
* Notify that a media state for a given location has changed. Should only be called from Media
* hosts themselves.
*/
fun updateHostState(
@MediaLocation location: Int,
hostState: MediaHostState
) = traceSection("MediaHostStatesManager#updateHostState") {
val currentState = mediaHostStates.get(location)
if (!hostState.equals(currentState)) {
val newState = hostState.copy()
mediaHostStates.put(location, newState)
updateCarouselDimensions(location, hostState)
// First update all the controllers to ensure they get the chance to measure
for (controller in controllers) {
controller.stateCallback.onHostStateChanged(location, newState)
}
fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) =
traceSection("MediaHostStatesManager#updateHostState") {
val currentState = mediaHostStates.get(location)
if (!hostState.equals(currentState)) {
val newState = hostState.copy()
mediaHostStates.put(location, newState)
updateCarouselDimensions(location, hostState)
// First update all the controllers to ensure they get the chance to measure
for (controller in controllers) {
controller.stateCallback.onHostStateChanged(location, newState)
}
// Then update all other callbacks which may depend on the controllers above
for (callback in callbacks) {
callback.onHostStateChanged(location, newState)
// Then update all other callbacks which may depend on the controllers above
for (callback in callbacks) {
callback.onHostStateChanged(location, newState)
}
}
}
}
/**
* Get the dimensions of all players combined, which determines the overall height of the
* media carousel and the media hosts.
* Get the dimensions of all players combined, which determines the overall height of the media
* carousel and the media hosts.
*/
fun updateCarouselDimensions(
@MediaLocation location: Int,
hostState: MediaHostState
): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") {
val result = MeasurementOutput(0, 0)
for (controller in controllers) {
val measurement = controller.getMeasurementsForState(hostState)
measurement?.let {
if (it.measuredHeight > result.measuredHeight) {
result.measuredHeight = it.measuredHeight
}
if (it.measuredWidth > result.measuredWidth) {
result.measuredWidth = it.measuredWidth
): MeasurementOutput =
traceSection("MediaHostStatesManager#updateCarouselDimensions") {
val result = MeasurementOutput(0, 0)
for (controller in controllers) {
val measurement = controller.getMeasurementsForState(hostState)
measurement?.let {
if (it.measuredHeight > result.measuredHeight) {
result.measuredHeight = it.measuredHeight
}
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) {
callbacks.add(callback)
}
/**
* Remove a callback that listens to media states
*/
/** Remove a callback that listens to media states */
fun removeCallback(callback: Callback) {
callbacks.remove(callback)
}
/**
* Register a controller that listens to media states and is used to determine the size of
* the media carousel
* Register a controller that listens to media states and is used to determine the size of the
* media carousel
*/
fun addController(controller: MediaViewController) {
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) {
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
import android.content.Context
@@ -11,14 +27,13 @@ import com.android.systemui.Gefingerpoken
import com.android.wm.shell.animation.physicsAnimator
/**
* A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful
* when only measuring children but not the parent, when trying to apply a new scroll position
* A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when
* only measuring children but not the parent, when trying to apply a new scroll position
*/
class MediaScrollView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : HorizontalScrollView(context, attrs, defStyleAttr) {
class MediaScrollView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
HorizontalScrollView(context, attrs, defStyleAttr) {
lateinit var contentContainer: ViewGroup
private set
@@ -33,35 +48,33 @@ class MediaScrollView @JvmOverloads constructor(
* Get the current content translation. This is usually the normal translationX of the content,
* but when animating, it might differ
*/
fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) {
animationTargetX
} else {
contentContainer.translationX
}
fun getContentTranslation() =
if (contentContainer.physicsAnimator.isRunning()) {
animationTargetX
} else {
contentContainer.translationX
}
/**
* 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
* is always absolute. This function is its own inverse.
* carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is
* always absolute. This function is its own inverse.
*/
private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) {
contentContainer.width - width - scrollX
} else {
scrollX
}
private fun transformScrollX(scrollX: Int): Int =
if (isLayoutRtl) {
contentContainer.width - width - 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
get() = transformScrollX(scrollX)
set(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) {
if (mScrollX != x || mScrollY != y) {
val oldX: Int = mScrollX
@@ -78,17 +91,13 @@ class MediaScrollView @JvmOverloads constructor(
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false
touchListener?.let {
intercept = it.onInterceptTouchEvent(ev)
}
touchListener?.let { intercept = it.onInterceptTouchEvent(ev) }
return super.onInterceptTouchEvent(ev) || intercept
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
var touch = false
touchListener?.let {
touch = it.onTouchEvent(ev)
}
touchListener?.let { touch = it.onTouchEvent(ev) }
return super.onTouchEvent(ev) || touch
}
@@ -112,19 +121,25 @@ class MediaScrollView @JvmOverloads constructor(
// When we're dismissing we ignore all the scrolling
return false
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
return super.overScrollBy(
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() {
val now = SystemClock.uptimeMillis()
val event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
event.source = InputDevice.SOURCE_TOUCHSCREEN
super.onTouchEvent(event)
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
* 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 configurationController: ConfigurationController,
private val mediaHostStatesManager: MediaHostStatesManager,
@@ -50,17 +52,18 @@ class MediaViewController @Inject constructor(
) {
/**
* Indicating that the media view controller is for a notification-based player,
* session-based player, or recommendation
* Indicating that the media view controller is for a notification-based player, session-based
* player, or recommendation
*/
enum class TYPE {
PLAYER, RECOMMENDATION
PLAYER,
RECOMMENDATION
}
companion object {
@JvmField
val GUTS_ANIMATION_DURATION = 500L
val controlIds = setOf(
@JvmField val GUTS_ANIMATION_DURATION = 500L
val controlIds =
setOf(
R.id.media_progress_bar,
R.id.actionNext,
R.id.actionPrev,
@@ -71,22 +74,20 @@ class MediaViewController @Inject constructor(
R.id.action4,
R.id.media_scrubbing_elapsed_time,
R.id.media_scrubbing_total_time
)
)
val detailIds = setOf(
val detailIds =
setOf(
R.id.header_title,
R.id.header_artist,
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
private var firstRefresh: Boolean = true
@VisibleForTesting
private var transitionLayout: TransitionLayout? = null
@VisibleForTesting private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController()
private var animationDelay: Long = 0
private var animationDuration: Long = 0
@@ -94,116 +95,98 @@ class MediaViewController @Inject constructor(
private val measurement = MeasurementOutput(0, 0)
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()
/**
* The ending location of the view where it ends when all animations and transitions have
* finished
*/
@MediaLocation
var currentEndLocation: Int = -1
@MediaLocation var currentEndLocation: Int = -1
/**
* The starting location of the view where it starts for all animations and transitions
*/
@MediaLocation
private var currentStartLocation: Int = -1
/** The starting location of the view where it starts for all animations and transitions */
@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
/**
* A temporary state used to store intermediate measurements.
*/
/** A temporary state used to store intermediate measurements. */
private val tmpState = TransitionViewState()
/**
* A temporary state used to store intermediate measurements.
*/
/** A temporary state used to store intermediate measurements. */
private val tmpState2 = TransitionViewState()
/**
* A temporary state used to store intermediate measurements.
*/
/** A temporary state used to store intermediate measurements. */
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()
/**
* The current width of the player. This might not factor in case the player is animating
* to the current state, but represents the end state
* The current width of the player. This might not factor in case the player is animating to the
* current state, but represents the end state
*/
var currentWidth: Int = 0
/**
* The current height of the player. This might not factor in case the player is animating
* to the current state, but represents the end state
* The current height of the player. This might not factor in case the player is animating to
* the current state, but represents the end state
*/
var currentHeight: Int = 0
/**
* Get the translationX of the layout
*/
/** Get the translationX of the layout */
var translationX: Float = 0.0f
private set
get() {
return transitionLayout?.translationX ?: 0.0f
}
/**
* Get the translationY of the layout
*/
/** Get the translationY of the layout */
var translationY: Float = 0.0f
private set
get() {
return transitionLayout?.translationY ?: 0.0f
}
/**
* A callback for RTL config changes
*/
private val configurationListener = object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
// Because the TransitionLayout is not always attached (and calculates/caches layout
// results regardless of attach state), we have to force the layoutDirection of the view
// to the correct value for the user's current locale to ensure correct recalculation
// when/after calling refreshState()
newConfig?.apply {
if (transitionLayout?.rawLayoutDirection != layoutDirection) {
transitionLayout?.layoutDirection = layoutDirection
refreshState()
/** A callback for RTL config changes */
private val configurationListener =
object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
// Because the TransitionLayout is not always attached (and calculates/caches layout
// results regardless of attach state), we have to force the layoutDirection of the
// view
// to the correct value for the user's current locale to ensure correct
// recalculation
// when/after calling refreshState()
newConfig?.apply {
if (transitionLayout?.rawLayoutDirection != layoutDirection) {
transitionLayout?.layoutDirection = layoutDirection
refreshState()
}
}
}
}
}
/**
* A callback for media state changes
*/
val stateCallback = object : MediaHostStatesManager.Callback {
override fun onHostStateChanged(
@MediaLocation location: Int,
mediaHostState: MediaHostState
) {
if (location == currentEndLocation || location == currentStartLocation) {
setCurrentState(currentStartLocation,
/** A callback for media state changes */
val stateCallback =
object : MediaHostStatesManager.Callback {
override fun onHostStateChanged(
@MediaLocation location: Int,
mediaHostState: MediaHostState
) {
if (location == currentEndLocation || location == currentStartLocation) {
setCurrentState(
currentStartLocation,
currentEndLocation,
currentTransitionProgress,
applyImmediately = false)
applyImmediately = false
)
}
}
}
}
/**
* The expanded constraint set used to render a expanded player. If it is modified, make sure
* to call [refreshState]
* The expanded constraint set used to render a expanded player. If it is modified, make sure to
* call [refreshState]
*/
val collapsedLayout = ConstraintSet()
@@ -213,9 +196,7 @@ class MediaViewController @Inject constructor(
*/
val expandedLayout = ConstraintSet()
/**
* Whether the guts are visible for the associated player.
*/
/** Whether the guts are visible for the associated player. */
var isGutsVisible = false
private set
@@ -237,17 +218,17 @@ class MediaViewController @Inject constructor(
configurationController.removeCallback(configurationListener)
}
/**
* Show guts with an animated transition.
*/
/** Show guts with an animated transition. */
fun openGuts() {
if (isGutsVisible) return
isGutsVisible = true
animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
setCurrentState(currentStartLocation,
currentEndLocation,
currentTransitionProgress,
applyImmediately = false)
setCurrentState(
currentStartLocation,
currentEndLocation,
currentTransitionProgress,
applyImmediately = false
)
}
/**
@@ -262,10 +243,12 @@ class MediaViewController @Inject constructor(
if (!immediate) {
animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
}
setCurrentState(currentStartLocation,
currentEndLocation,
currentTransitionProgress,
applyImmediately = immediate)
setCurrentState(
currentStartLocation,
currentEndLocation,
currentTransitionProgress,
applyImmediately = immediate
)
}
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 =
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
* [TransitionViewState].
*/
private fun setGutsViewState(viewState: TransitionViewState) {
val controlsIds = when (type) {
TYPE.PLAYER -> MediaViewHolder.controlsIds
TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
}
val controlsIds =
when (type) {
TYPE.PLAYER -> MediaViewHolder.controlsIds
TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
}
val gutsIds = GutsViewHolder.ids
controlsIds.forEach { id ->
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(
viewState: TransitionViewState,
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
* it's not available, it will recreate one by measuring, which may be expensive.
*/
@VisibleForTesting
fun obtainViewState(state: MediaHostState?): TransitionViewState? {
@VisibleForTesting
fun obtainViewState(state: MediaHostState?): TransitionViewState? {
if (state == null || state.measurementInput == null) {
return null
}
@@ -371,10 +351,12 @@ class MediaViewController @Inject constructor(
}
// Let's create a new measurement
if (state.expansion == 0.0f || state.expansion == 1.0f) {
result = transitionLayout!!.calculateViewState(
result =
transitionLayout!!.calculateViewState(
state.measurementInput!!,
constraintSetForExpansion(state.expansion),
TransitionViewState())
TransitionViewState()
)
setGutsViewState(result)
// 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 endState = state.copy().also { it.expansion = 1.0f }
val endViewState = obtainViewState(endState) as TransitionViewState
result = layoutController.getInterpolatedState(
startViewState,
endViewState,
state.expansion)
result =
layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
}
if (state.squishFraction <= 1f) {
return squishViewState(result, state.squishFraction)
@@ -401,11 +381,7 @@ class MediaViewController @Inject constructor(
return result
}
private fun getKey(
state: MediaHostState,
guts: Boolean,
result: CacheKey
): CacheKey {
private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
result.apply {
heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 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
* and should therefore be done carefully.
* Attach a view to this controller. This may perform measurements if it's not available yet and
* should therefore be done carefully.
*/
fun attach(
transitionLayout: TransitionLayout,
type: TYPE
) = traceSection("MediaViewController#attach") {
updateMediaViewControllerType(type)
logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
this.transitionLayout = transitionLayout
layoutController.attach(transitionLayout)
if (currentEndLocation == -1) {
return
}
// Set the previously set state immediately to the view, now that it's finally attached
setCurrentState(
fun attach(transitionLayout: TransitionLayout, type: TYPE) =
traceSection("MediaViewController#attach") {
updateMediaViewControllerType(type)
logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
this.transitionLayout = transitionLayout
layoutController.attach(transitionLayout)
if (currentEndLocation == -1) {
return
}
// Set the previously set state immediately to the view, now that it's finally attached
setCurrentState(
startLocation = currentStartLocation,
endLocation = currentEndLocation,
transitionProgress = currentTransitionProgress,
applyImmediately = true)
}
applyImmediately = true
)
}
/**
* Obtain a measurement for a given location. This makes sure that the state is up to date
* and all widgets know their location. Calling this method may create a measurement if we
* don't have a cached value available already.
* Obtain a measurement for a given location. This makes sure that the state is up to date and
* all widgets know their location. Calling this method may create a measurement if we don't
* have a cached value available already.
*/
fun getMeasurementsForState(
hostState: MediaHostState
): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") {
val viewState = obtainViewState(hostState) ?: return null
measurement.measuredWidth = viewState.width
measurement.measuredHeight = viewState.height
return measurement
}
fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? =
traceSection("MediaViewController#getMeasurementsForState") {
val viewState = obtainViewState(hostState) ?: return null
measurement.measuredWidth = viewState.width
measurement.measuredHeight = viewState.height
return measurement
}
/**
* 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,
transitionProgress: Float,
applyImmediately: Boolean
) = traceSection("MediaViewController#setCurrentState") {
currentEndLocation = endLocation
currentStartLocation = startLocation
currentTransitionProgress = transitionProgress
logger.logMediaLocation("setCurrentState", startLocation, endLocation)
) =
traceSection("MediaViewController#setCurrentState") {
currentEndLocation = endLocation
currentStartLocation = startLocation
currentTransitionProgress = transitionProgress
logger.logMediaLocation("setCurrentState", startLocation, endLocation)
val shouldAnimate = animateNextStateChange && !applyImmediately
val shouldAnimate = animateNextStateChange && !applyImmediately
val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
// 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
// reset once the state is fully available
var endViewState = obtainViewState(endHostState) ?: return
endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
layoutController.setMeasureState(endViewState)
// 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
// reset once the state is fully available
var endViewState = obtainViewState(endHostState) ?: return
endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
layoutController.setMeasureState(endViewState)
// If the view isn't bound, we can drop the animation, otherwise we'll execute it
animateNextStateChange = false
if (transitionLayout == null) {
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)
// If the view isn't bound, we can drop the animation, otherwise we'll execute it
animateNextStateChange = false
if (transitionLayout == null) {
return
}
} 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)
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.
// 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(
viewState: TransitionViewState?,
@@ -558,8 +550,8 @@ class MediaViewController @Inject constructor(
}
/**
* Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation].
* In the event of [location] not being visible, [locationWhenHidden] will be used instead.
* Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event
* of [location] not being visible, [locationWhenHidden] will be used instead.
*
* @param location Target
* @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.
*/
fun onLocationPreChange(@MediaLocation newLocation: Int) {
obtainViewStateForLocation(newLocation)?.let {
layoutController.setMeasureState(it)
}
obtainViewStateForLocation(newLocation)?.let { 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) {
animateNextStateChange = true
animationDuration = duration
animationDelay = delay
}
/**
* Clear all existing measurements and refresh the state to match the view.
*/
fun refreshState() = traceSection("MediaViewController#refreshState") {
// Let's clear all of our measurements and recreate them!
viewStates.clear()
if (firstRefresh) {
// This is the first bind, let's ensure we pre-cache all measurements. Otherwise
// We'll just load these on demand.
ensureAllMeasurements()
firstRefresh = false
/** Clear all existing measurements and refresh the state to match the view. */
fun refreshState() =
traceSection("MediaViewController#refreshState") {
// Let's clear all of our measurements and recreate them!
viewStates.clear()
if (firstRefresh) {
// This is the first bind, let's ensure we pre-cache all measurements. Otherwise
// We'll just load these on demand.
ensureAllMeasurements()
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(
var widthMeasureSpec: Int = -1,
var heightMeasureSpec: Int = -1,

View File

@@ -24,40 +24,32 @@ import javax.inject.Inject
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
class MediaViewLogger @Inject constructor(
@MediaViewLog private val buffer: LogBuffer
) {
class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogBuffer) {
fun logMediaSize(reason: String, width: Int, height: Int) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = reason
int1 = width
int2 = height
},
{
"size ($str1): $int1 x $int2"
}
TAG,
LogLevel.DEBUG,
{
str1 = reason
int1 = width
int2 = height
},
{ "size ($str1): $int1 x $int2" }
)
}
fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = reason
int1 = startLocation
int2 = endLocation
},
{
"location ($str1): $int1 -> $int2"
}
TAG,
LogLevel.DEBUG,
{
str1 = reason
int1 = startLocation
int2 = endLocation
},
{ "location ($str1): $int1 -> $int2" }
)
}
}
}

View File

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

View File

@@ -30,17 +30,13 @@ import javax.inject.Inject
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
class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) {
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 {
return instanceIdSequence.newInstanceId()
}
@@ -51,12 +47,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
instanceId: InstanceId,
playbackLocation: Int
) {
val event = when (playbackLocation) {
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
else -> throw IllegalArgumentException("Unknown playback location")
}
val event =
when (playbackLocation) {
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
else -> throw IllegalArgumentException("Unknown playback location")
}
logger.logWithInstanceId(event, uid, packageName, instanceId)
}
@@ -66,12 +63,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
instanceId: InstanceId,
playbackLocation: Int
) {
val event = when (playbackLocation) {
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
else -> throw IllegalArgumentException("Unknown playback location")
}
val event =
when (playbackLocation) {
MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
else -> throw IllegalArgumentException("Unknown playback location")
}
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) {
logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName,
instanceId)
logger.logWithInstanceId(
MediaUiEvent.OPEN_SETTINGS_LONG_PRESS,
uid,
packageName,
instanceId
)
}
fun logCarouselSettings() {
@@ -120,12 +122,13 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
}
fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) {
val event = when (buttonId) {
R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
else -> MediaUiEvent.TAP_ACTION_OTHER
}
val event =
when (buttonId) {
R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
else -> MediaUiEvent.TAP_ACTION_OTHER
}
logger.logWithInstanceId(event, uid, packageName, instanceId)
}
@@ -143,148 +146,130 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger)
}
fun logCarouselPosition(@MediaLocation location: Int) {
val event = when (location) {
MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
MediaHierarchyManager.LOCATION_LOCKSCREEN ->
MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
else -> throw IllegalArgumentException("Unknown media carousel location $location")
}
val event =
when (location) {
MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
MediaHierarchyManager.LOCATION_LOCKSCREEN ->
MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
else -> throw IllegalArgumentException("Unknown media carousel location $location")
}
logger.log(event)
}
fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName,
instanceId)
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_ADDED,
0,
packageName,
instanceId
)
}
fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName,
instanceId)
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED,
0,
packageName,
instanceId
)
}
fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName,
instanceId)
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED,
uid,
packageName,
instanceId
)
}
fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0,
packageName, instanceId, position)
logger.logWithInstanceIdAndPosition(
MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP,
0,
packageName,
instanceId,
position
)
}
fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName,
instanceId)
logger.logWithInstanceId(
MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP,
0,
packageName,
instanceId
)
}
fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) {
logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName,
instanceId)
logger.logWithInstanceId(
MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG,
uid,
packageName,
instanceId
)
}
}
enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "A new media control was added for media playing locally on the device")
LOCAL_MEDIA_ADDED(1029),
@UiEvent(doc = "A new media control was added for media cast from the device")
CAST_MEDIA_ADDED(1030),
@UiEvent(doc = "A new media control was added for media playing remotely")
REMOTE_MEDIA_ADDED(1031),
@UiEvent(doc = "The media for an existing control was transferred to local playback")
TRANSFER_TO_LOCAL(1032),
@UiEvent(doc = "The media for an existing control was transferred to a cast device")
TRANSFER_TO_CAST(1033),
@UiEvent(doc = "The media for an existing control was transferred to a remote device")
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")
ACTIVE_TO_RESUME(1014),
@UiEvent(doc = "A media control timed out")
MEDIA_TIMEOUT(1015),
@UiEvent(doc = "A media control was removed from the carousel")
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 = "A media control timed out") MEDIA_TIMEOUT(1015),
@UiEvent(doc = "A media control was removed from the carousel") 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")
DISMISS_LONG_PRESS(1020),
@UiEvent(doc = "The user opened media settings from a media control's long press menu")
OPEN_SETTINGS_LONG_PRESS(1021),
@UiEvent(doc = "The user opened media settings from the media carousel")
OPEN_SETTINGS_CAROUSEL(1022),
@UiEvent(doc = "The play/pause button on a media control was tapped")
TAP_ACTION_PLAY_PAUSE(1023),
@UiEvent(doc = "The previous button on a media control was tapped")
TAP_ACTION_PREV(1024),
@UiEvent(doc = "The next button on a media control was tapped")
TAP_ACTION_NEXT(1025),
@UiEvent(doc = "The previous button on a media control was tapped") 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")
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")
OPEN_OUTPUT_SWITCHER(1028),
@UiEvent(doc = "The user tapped on a media control view")
MEDIA_TAP_CONTENT_VIEW(1036),
@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 user tapped on a media control view") MEDIA_TAP_CONTENT_VIEW(1036),
@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")
MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
@UiEvent(doc = "The media carousel moved to the dream state")
MEDIA_CAROUSEL_LOCATION_DREAM(1040),
@UiEvent(doc = "A media recommendation card was added to the media carousel")
MEDIA_RECOMMENDATION_ADDED(1041),
@UiEvent(doc = "A media recommendation card was removed from the media carousel")
MEDIA_RECOMMENDATION_REMOVED(1042),
@UiEvent(doc = "An existing media control was made active as a recommendation")
MEDIA_RECOMMENDATION_ACTIVATED(1043),
@UiEvent(doc = "User tapped on an item in a media recommendation card")
MEDIA_RECOMMENDATION_ITEM_TAP(1044),
@UiEvent(doc = "User tapped on a media recommendation card")
MEDIA_RECOMMENDATION_CARD_TAP(1045),
@UiEvent(doc = "User opened the broadcast dialog from a media control")
MEDIA_OPEN_BROADCAST_DIALOG(1079);
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
import com.android.internal.logging.InstanceId
@@ -5,24 +21,26 @@ import com.android.systemui.media.controls.models.player.MediaData
class MediaTestUtils {
companion object {
val emptyMediaData = MediaData(
userId = 0,
initialized = true,
app = null,
appIcon = null,
artist = null,
song = null,
artwork = null,
actions = emptyList(),
actionsToShowInCompact = emptyList(),
packageName = "",
token = null,
clickIntent = null,
device = null,
active = true,
resumeAction = null,
isPlaying = false,
instanceId = InstanceId.fakeInstanceId(-1),
appUid = -1)
val emptyMediaData =
MediaData(
userId = 0,
initialized = true,
app = null,
appIcon = null,
artist = null,
song = null,
artwork = null,
actions = emptyList(),
actionsToShowInCompact = emptyList(),
packageName = "",
token = null,
clickIntent = null,
device = null,
active = true,
resumeAction = null,
isPlaying = false,
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
import android.testing.AndroidTestingRunner
@@ -21,4 +37,4 @@ class MediaViewHolderTest : SysuiTestCase() {
MediaViewHolder.create(inflater, parent)
}
}
}

View File

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

View File

@@ -57,17 +57,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
private lateinit var viewModel: SeekBarViewModel
private lateinit var fakeExecutor: FakeExecutor
private val taskExecutor: TaskExecutor = object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
private val taskExecutor: TaskExecutor =
object : TaskExecutor() {
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 mockTransport: MediaController.TransportControls
@Mock private lateinit var falsingManager: FalsingManager
@@ -81,7 +82,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun setUp() {
fakeExecutor = FakeExecutor(FakeSystemClock())
viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
viewModel.logSeek = { }
viewModel.logSeek = {}
whenever(mockController.sessionToken).thenReturn(token1)
whenever(mockBar.context).thenReturn(context)
@@ -135,16 +136,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationWithPlayback() {
// GIVEN that the duration is contained within the metadata
val duration = 12000L
val metadata = MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
val metadata =
MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -158,10 +161,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationWithoutPlayback() {
// GIVEN that the duration is contained within the metadata
val duration = 12000L
val metadata = MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
val metadata =
MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
whenever(mockController.getMetadata()).thenReturn(metadata)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -174,16 +178,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationNegative() {
// GIVEN that the duration is negative
val duration = -1L
val metadata = MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
val metadata =
MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -195,16 +201,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateDurationZero() {
// GIVEN that the duration is zero
val duration = 0L
val metadata = MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
val metadata =
MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
build()
}
whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -218,10 +226,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN that the metadata is null
whenever(mockController.getMetadata()).thenReturn(null)
// AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -233,10 +242,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
fun updateElapsedTime() {
// GIVEN that the PlaybackState contains the current position
val position = 200L
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, position, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, position, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -248,10 +258,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore
fun updateSeekAvailable() {
// GIVEN that seek is included in actions
val state = PlaybackState.Builder().run {
setActions(PlaybackState.ACTION_SEEK_TO)
build()
}
val state =
PlaybackState.Builder().run {
setActions(PlaybackState.ACTION_SEEK_TO)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -263,10 +274,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore
fun updateSeekNotAvailable() {
// GIVEN that seek is not included in actions
val state = PlaybackState.Builder().run {
setActions(PlaybackState.ACTION_PLAY)
build()
}
val state =
PlaybackState.Builder().run {
setActions(PlaybackState.ACTION_PLAY)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -318,9 +330,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore
fun onSeekProgressWithSeekStarting() {
val pos = 42L
with(viewModel) {
onSeekProgress(pos)
}
with(viewModel) { onSeekProgress(pos) }
fakeExecutor.runAllReady()
// THEN then elapsed time should not be updated
assertThat(viewModel.progress.value!!.elapsedTime).isNull()
@@ -329,11 +339,12 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test
fun seekStarted_listenerNotified() {
var isScrubbing: Boolean? = null
val listener = object : SeekBarViewModel.ScrubbingChangeListener {
override fun onScrubbingChanged(scrubbing: Boolean) {
isScrubbing = scrubbing
val listener =
object : SeekBarViewModel.ScrubbingChangeListener {
override fun onScrubbingChanged(scrubbing: Boolean) {
isScrubbing = scrubbing
}
}
}
viewModel.setScrubbingChangeListener(listener)
viewModel.onSeekStarting()
@@ -345,11 +356,12 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test
fun seekEnded_listenerNotified() {
var isScrubbing: Boolean? = null
val listener = object : SeekBarViewModel.ScrubbingChangeListener {
override fun onScrubbingChanged(scrubbing: Boolean) {
isScrubbing = scrubbing
val listener =
object : SeekBarViewModel.ScrubbingChangeListener {
override fun onScrubbingChanged(scrubbing: Boolean) {
isScrubbing = scrubbing
}
}
}
viewModel.setScrubbingChangeListener(listener)
// Start seeking
@@ -385,9 +397,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
val bar = SeekBar(context)
// WHEN we get an onProgressChanged event without an onStartTrackingTouch event
with(viewModel.seekBarListener) {
onProgressChanged(bar, pos, true)
}
with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) }
fakeExecutor.runAllReady()
// THEN we immediately update the transport
@@ -412,9 +422,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
viewModel.updateController(mockController)
// WHEN user starts dragging the seek bar
val pos = 42
val bar = SeekBar(context).apply {
progress = pos
}
val bar = SeekBar(context).apply { progress = pos }
viewModel.seekBarListener.onStartTrackingTouch(bar)
fakeExecutor.runAllReady()
// THEN transport controls should be used
@@ -427,9 +435,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
viewModel.updateController(mockController)
// WHEN user ends drag
val pos = 42
val bar = SeekBar(context).apply {
progress = pos
}
val bar = SeekBar(context).apply { progress = pos }
viewModel.seekBarListener.onStopTrackingTouch(bar)
fakeExecutor.runAllReady()
// THEN transport controls should be used
@@ -443,9 +449,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// WHEN user starts dragging the seek bar
val pos = 42
val progPos = 84
val bar = SeekBar(context).apply {
progress = pos
}
val bar = SeekBar(context).apply { progress = pos }
with(viewModel.seekBarListener) {
onStartTrackingTouch(bar)
onProgressChanged(bar, progPos, true)
@@ -478,10 +482,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test
fun queuePollTaskWhenPlaying() {
// GIVEN that the track is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN the controller is updated
viewModel.updateController(mockController)
@@ -492,10 +497,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test
fun noQueuePollTaskWhenStopped() {
// GIVEN that the playback state is stopped
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN updated
viewModel.updateController(mockController)
@@ -512,10 +518,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
runAllReady()
}
// AND the playback state is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN updated
viewModel.updateController(mockController)
@@ -532,10 +539,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
runAllReady()
}
// AND the playback state is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// WHEN updated
viewModel.updateController(mockController)
@@ -546,10 +554,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Test
fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
// GIVEN that the track is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController)
// WHEN the next task runs
@@ -566,10 +575,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN listening
viewModel.listening = true
// AND the playback state is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController)
with(fakeExecutor) {
@@ -592,10 +602,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN listening
viewModel.listening = true
// AND the playback state is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController)
with(fakeExecutor) {
@@ -621,10 +632,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
// GIVEN listening
viewModel.listening = true
// AND the playback state is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController)
with(fakeExecutor) {
@@ -654,10 +666,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
runAllReady()
}
// AND the playback state is playing
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
viewModel.updateController(mockController)
// WHEN start listening
@@ -673,10 +686,11 @@ public class SeekBarViewModelTest : SysuiTestCase() {
verify(mockController).registerCallback(captor.capture())
val callback = captor.value
// WHEN the callback receives an new state
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
build()
}
callback.onPlaybackStateChanged(state)
with(fakeExecutor) {
advanceClockToNext()
@@ -690,16 +704,18 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Ignore
fun clearSeekBar() {
// GIVEN that the duration is contained within the metadata
val metadata = MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
build()
}
val metadata =
MediaMetadata.Builder().run {
putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
build()
}
whenever(mockController.getMetadata()).thenReturn(metadata)
// AND a valid playback state (ie. media session is not destroyed)
val state = PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
val state =
PlaybackState.Builder().run {
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
build()
}
whenever(mockController.getPlaybackState()).thenReturn(state)
// AND the controller has been updated
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
import android.app.smartspace.SmartspaceAction
@@ -36,11 +52,11 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
@Test
fun isValid_tooFewRecs_returnsFalse() {
val data = DEFAULT_DATA.copy(
recommendations = listOf(
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
val data =
DEFAULT_DATA.copy(
recommendations =
listOf(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
)
)
assertThat(data.isValid()).isFalse()
}
@@ -50,14 +66,10 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
val recommendations = mutableListOf<SmartspaceAction>()
// Add one fewer recommendation w/ icon than the number required
for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) {
recommendations.add(
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
)
recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
}
for (i in 1 until 3) {
recommendations.add(
SmartspaceAction.Builder("id", "title").setIcon(null).build()
)
recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(null).build())
}
val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -70,9 +82,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
val recommendations = mutableListOf<SmartspaceAction>()
// Add the number of required recommendations
for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) {
recommendations.add(
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
)
recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
}
val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -85,9 +95,7 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
val recommendations = mutableListOf<SmartspaceAction>()
// Add more than enough recommendations
for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) {
recommendations.add(
SmartspaceAction.Builder("id", "title").setIcon(icon).build()
)
recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
}
val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -96,13 +104,14 @@ class SmartspaceMediaDataTest : SysuiTestCase() {
}
}
private val DEFAULT_DATA = SmartspaceMediaData(
targetId = "INVALID",
isActive = false,
packageName = "INVALID",
cardAction = null,
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
instanceId = InstanceId.fakeInstanceId(-1)
)
private val DEFAULT_DATA =
SmartspaceMediaData(
targetId = "INVALID",
isActive = false,
packageName = "INVALID",
cardAction = null,
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
instanceId = InstanceId.fakeInstanceId(-1)
)

View File

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

View File

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

View File

@@ -23,14 +23,12 @@ import android.media.session.MediaSessionManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -44,17 +42,15 @@ import org.mockito.Mockito.any
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
private const val PACKAGE = "PKG"
private const val KEY = "TEST_KEY"
private const val NOTIF_KEY = "TEST_KEY"
private val info = MediaTestUtils.emptyMediaData.copy(
packageName = PACKAGE,
notificationKey = NOTIF_KEY
)
private val info =
MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY)
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -141,10 +137,10 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
// Capture listener
bgExecutor.runAllReady()
val listenerCaptor = ArgumentCaptor.forClass(
MediaSessionManager.OnActiveSessionsChangedListener::class.java)
verify(mediaSessionManager).addOnActiveSessionsChangedListener(
listenerCaptor.capture(), any())
val listenerCaptor =
ArgumentCaptor.forClass(MediaSessionManager.OnActiveSessionsChangedListener::class.java)
verify(mediaSessionManager)
.addOnActiveSessionsChangedListener(listenerCaptor.capture(), any())
sessionListener = listenerCaptor.value
filter.addListener(mediaListener)
@@ -163,8 +159,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
filter.onMediaDataLoaded(KEY, null, mediaData1)
bgExecutor.runAllReady()
fgExecutor.runAllReady()
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
}
@Test
@@ -186,8 +182,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
}
@Test
@@ -216,8 +212,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
}
@Test
@@ -232,15 +228,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the local session
filter.onMediaDataLoaded(KEY, null, mediaData2)
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is filtered
verify(mediaListener, never()).onMediaDataLoaded(
eq(KEY), eq(null), eq(mediaData2), anyBoolean(), anyInt(), anyBoolean())
verify(mediaListener, never())
.onMediaDataLoaded(
eq(KEY),
eq(null),
eq(mediaData2),
anyBoolean(),
anyInt(),
anyBoolean()
)
}
@Test
@@ -256,8 +259,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
fgExecutor.runAllReady()
// THEN the event is not filtered because there isn't a notification for the remote
// session.
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
}
@Test
@@ -274,16 +277,22 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the local session
filter.onMediaDataLoaded(key2, null, mediaData2)
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is filtered
verify(mediaListener, never())
.onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(),
anyInt(), anyBoolean())
.onMediaDataLoaded(
eq(key2),
eq(null),
eq(mediaData2),
anyBoolean(),
anyInt(),
anyBoolean()
)
// AND there should be a removed event for key2
verify(mediaListener).onMediaDataRemoved(eq(key2))
}
@@ -302,15 +311,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the remote session
filter.onMediaDataLoaded(key2, null, mediaData2)
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
}
@Test
@@ -326,15 +335,15 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
// WHEN a loaded event is received that matches the local session
filter.onMediaDataLoaded(KEY, null, mediaData2)
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
}
@Test
@@ -352,8 +361,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the event is not filtered
verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
}
@Test
@@ -375,8 +384,8 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the key migration event is fired
verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true),
eq(0), eq(false))
verify(mediaListener)
.onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), eq(0), eq(false))
}
@Test
@@ -406,14 +415,20 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() {
fgExecutor.runAllReady()
// THEN the key migration event is filtered
verify(mediaListener, never())
.onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(),
anyInt(), anyBoolean())
.onMediaDataLoaded(
eq(key2),
eq(null),
eq(mediaData2),
anyBoolean(),
anyInt(),
anyBoolean()
)
// WHEN a loaded event is received that matches the remote session
filter.onMediaDataLoaded(key2, null, mediaData1)
bgExecutor.runAllReady()
fgExecutor.runAllReady()
// THEN the key migration event is fired
verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true),
eq(0), eq(false))
verify(mediaListener)
.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.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY"
@@ -73,7 +73,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
@Captor private lateinit var dozingCallbackCaptor:
@Captor
private lateinit var dozingCallbackCaptor:
ArgumentCaptor<StatusBarStateController.StateListener>
@JvmField @Rule val mockito = MockitoJUnit.rule()
private lateinit var metadataBuilder: MediaMetadata.Builder
@@ -88,36 +89,41 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun setup() {
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
executor = FakeExecutor(clock)
mediaTimeoutListener = MediaTimeoutListener(
mediaControllerFactory,
executor,
logger,
statusBarStateController,
clock
)
mediaTimeoutListener =
MediaTimeoutListener(
mediaControllerFactory,
executor,
logger,
statusBarStateController,
clock
)
mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback
// Create a media session and notification for testing.
metadataBuilder = MediaMetadata.Builder().apply {
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
playbackBuilder = PlaybackState.Builder().apply {
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
setActions(PlaybackState.ACTION_PLAY)
}
session = MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
metadataBuilder =
MediaMetadata.Builder().apply {
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
}
playbackBuilder =
PlaybackState.Builder().apply {
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
setActions(PlaybackState.ACTION_PLAY)
}
session =
MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
session.setActive(true)
mediaData = MediaTestUtils.emptyMediaData.copy(
app = PACKAGE,
packageName = PACKAGE,
token = session.sessionToken
)
mediaData =
MediaTestUtils.emptyMediaData.copy(
app = PACKAGE,
packageName = PACKAGE,
token = session.sessionToken
)
resumeData = mediaData.copy(token = null, active = false, resumption = true)
}
@@ -215,8 +221,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we're registered
testOnMediaDataLoaded_registersPlaybackListener()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
assertThat(executor.numPending()).isEqualTo(1)
assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
}
@@ -226,8 +233,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING, 0L, 0f).build())
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
)
assertThat(executor.numPending()).isEqualTo(0)
verify(logger).logTimeoutCancelled(eq(KEY), any())
}
@@ -237,8 +245,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_STOPPED, 0L, 0f).build())
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
)
assertThat(executor.numPending()).isEqualTo(1)
}
@@ -332,9 +341,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
// WHEN regular media is paused
val pausedState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
`when`(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1)
@@ -365,9 +373,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
// AND that media is resumed
val playingState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
`when`(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
@@ -389,15 +396,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
// Load media data once
val pausedState = PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE)
.build()
val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
loadMediaDataWithPlaybackState(pausedState)
// When media data is loaded again, with different actions
val playingState = PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PLAY)
.build()
val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
loadMediaDataWithPlaybackState(playingState)
// Then the callback is not invoked
@@ -407,15 +410,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
// Load media data once
val pausedState = PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE)
.build()
val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
loadMediaDataWithPlaybackState(pausedState)
// When the playback state changes, and has different actions
val playingState = PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PLAY)
.build()
val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
// Then the callback is invoked
@@ -424,24 +423,30 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
val customOne = PlaybackState.CustomAction.Builder(
val customOne =
PlaybackState.CustomAction.Builder(
"ACTION_1",
"custom action 1",
android.R.drawable.ic_media_ff)
android.R.drawable.ic_media_ff
)
.build()
val pausedState = PlaybackState.Builder()
val pausedState =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customOne)
.build()
loadMediaDataWithPlaybackState(pausedState)
// When the playback state actions change
val customTwo = PlaybackState.CustomAction.Builder(
"ACTION_2",
"custom action 2",
android.R.drawable.ic_media_rew)
val customTwo =
PlaybackState.CustomAction.Builder(
"ACTION_2",
"custom action 2",
android.R.drawable.ic_media_rew
)
.build()
val pausedStateTwoActions = PlaybackState.Builder()
val pausedStateTwoActions =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customOne)
.addCustomAction(customTwo)
@@ -454,9 +459,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnPlaybackStateChanged_sameActions_noCallback() {
val stateWithActions = PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PLAY)
.build()
val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
loadMediaDataWithPlaybackState(stateWithActions)
// When the playback state updates with the same actions
@@ -470,18 +473,20 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
val actionName = "custom action"
val actionIcon = android.R.drawable.ic_media_ff
val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
.build()
val stateOne = PlaybackState.Builder()
val customOne =
PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
val stateOne =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customOne)
.build()
loadMediaDataWithPlaybackState(stateOne)
// When the playback state is updated, but has the same actions
val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
.build()
val stateTwo = PlaybackState.Builder()
val customTwo =
PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
val stateTwo =
PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customTwo)
.build()
@@ -494,15 +499,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
// Load media data in paused state
val pausedState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
loadMediaDataWithPlaybackState(pausedState)
// When media data is loaded again but playing
val playingState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING, 0L, 1f)
.build()
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
loadMediaDataWithPlaybackState(playingState)
// Then the callback is not invoked
@@ -512,15 +515,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
// Load media data in paused state
val pausedState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
loadMediaDataWithPlaybackState(pausedState)
// When the playback state changes to playing
val playingState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING, 0L, 1f)
.build()
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
// Then the callback is invoked
@@ -530,15 +531,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
// Load media data in paused state
val pausedState = PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f)
.build()
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
loadMediaDataWithPlaybackState(pausedState)
// When the playback state is updated, but still not playing
val playingState = PlaybackState.Builder()
.setState(PlaybackState.STATE_STOPPED, 0L, 0f)
.build()
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
// Then the callback is not invoked
@@ -549,8 +548,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
// When paused media is loaded
testOnMediaDataLoaded_registersPlaybackListener()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
// And we doze past the scheduled timeout
@@ -574,8 +574,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time)
testOnMediaDataLoaded_registersPlaybackListener()
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
// 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.models.player.MediaData
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.RESUME_MEDIA_TIMEOUT
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.After
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
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 fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
private fun <T> any(): T = Mockito.any<T>()
@SmallTest
@@ -98,26 +100,32 @@ class MediaResumeListenerTest : SysuiTestCase() {
private lateinit var resumeListener: MediaResumeListener
private val clock = FakeSystemClock()
private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
private var originalQsSetting =
Settings.Global.getInt(
context.contentResolver,
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
1
)
private var originalResumeSetting =
Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
Settings.Global.putInt(context.contentResolver,
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
Settings.Secure.putInt(context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
Settings.Global.putInt(
context.contentResolver,
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
1
)
Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
.thenReturn(resumeBrowser)
.thenReturn(resumeBrowser)
// resume components are stored in sharedpreferences
whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
.thenReturn(sharedPrefs)
.thenReturn(sharedPrefs)
whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
@@ -125,36 +133,59 @@ class MediaResumeListenerTest : SysuiTestCase() {
whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
executor = FakeExecutor(clock)
resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
tunerService, resumeBrowserFactory, dumpManager, clock)
resumeListener =
MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener)
data = MediaTestUtils.emptyMediaData.copy(
data =
MediaTestUtils.emptyMediaData.copy(
song = TITLE,
packageName = PACKAGE_NAME,
token = token)
token = token
)
}
@After
fun tearDown() {
Settings.Global.putInt(context.contentResolver,
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting)
Settings.Secure.putInt(context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting)
Settings.Global.putInt(
context.contentResolver,
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
originalQsSetting
)
Settings.Secure.putInt(
context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME,
originalResumeSetting
)
}
@Test
fun testWhenNoResumption_doesNothing() {
Settings.Secure.putInt(context.contentResolver,
Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
// When listener is created, we do NOT register a user change listener
val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
resumeBrowserFactory, dumpManager, clock)
val listener =
MediaResumeListener(
context,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
listener.setManager(mediaDataManager)
verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
any(), any(), any(), anyInt(), any())
verify(broadcastDispatcher, never())
.registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any())
// When data is loaded, we do NOT execute or update anything
listener.onMediaDataLoaded(KEY, OLD_KEY, data)
@@ -175,9 +206,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
fun testOnLoad_checksForResume_badService() {
setUpMbsWithValidResolveInfo()
whenever(resumeBrowser.testConnection()).thenAnswer {
callbackCaptor.value.onError()
}
whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
// When media data is loaded that has not been checked yet, and does not have a MBS
resumeListener.onMediaDataLoaded(KEY, null, data)
@@ -231,7 +260,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
// But we do not tell it to add new controls
verify(mediaDataManager, never())
.addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
.addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
}
@Test
@@ -258,8 +287,15 @@ class MediaResumeListenerTest : SysuiTestCase() {
// Make sure broadcast receiver is registered
resumeListener.setManager(mediaDataManager)
verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver),
any(), any(), any(), anyInt(), any())
verify(broadcastDispatcher)
.registerReceiver(
eq(resumeListener.userChangeReceiver),
any(),
any(),
any(),
anyInt(),
any()
)
// When we get an unlock event
val intent = Intent(Intent.ACTION_USER_UNLOCKED)
@@ -269,8 +305,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
verify(resumeBrowser, times(3)).findRecentMedia()
// Then since the mock service found media, the manager should be informed
verify(mediaDataManager, times(3)).addResumptionControls(anyInt(),
any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
verify(mediaDataManager, times(3))
.addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
}
@Test
@@ -309,12 +345,14 @@ class MediaResumeListenerTest : SysuiTestCase() {
// Then we save an update with the current time
verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
.dropLastWhile { it.isEmpty() }.forEach {
val result = it.split("/")
assertThat(result.size).isEqualTo(3)
assertThat(result[2].toLong()).isEqualTo(currentTime)
}
componentCaptor.value
.split(ResumeMediaBrowser.DELIMITER.toRegex())
.dropLastWhile { it.isEmpty() }
.forEach {
val result = it.split("/")
assertThat(result.size).isEqualTo(3)
assertThat(result[2].toLong()).isEqualTo(currentTime)
}
verify(sharedPrefsEditor, times(1)).apply()
}
@@ -333,8 +371,16 @@ class MediaResumeListenerTest : SysuiTestCase() {
val lastPlayed = clock.currentTimeMillis()
val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
tunerService, resumeBrowserFactory, dumpManager, clock)
val resumeListener =
MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener)
@@ -344,8 +390,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
// We add its resume controls
verify(resumeBrowser, times(1)).findRecentMedia()
verify(mediaDataManager, times(1)).addResumptionControls(anyInt(),
any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
verify(mediaDataManager, times(1))
.addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
}
@Test
@@ -354,8 +400,16 @@ class MediaResumeListenerTest : SysuiTestCase() {
val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
tunerService, resumeBrowserFactory, dumpManager, clock)
val resumeListener =
MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener)
@@ -365,8 +419,8 @@ class MediaResumeListenerTest : SysuiTestCase() {
// We do not try to add resume controls
verify(resumeBrowser, times(0)).findRecentMedia()
verify(mediaDataManager, times(0)).addResumptionControls(anyInt(),
any(), any(), any(), any(), any(), any())
verify(mediaDataManager, times(0))
.addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
}
@Test
@@ -385,8 +439,16 @@ class MediaResumeListenerTest : SysuiTestCase() {
val lastPlayed = currentTime - 1000
val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
tunerService, resumeBrowserFactory, dumpManager, clock)
val resumeListener =
MediaResumeListener(
mockContext,
broadcastDispatcher,
executor,
tunerService,
resumeBrowserFactory,
dumpManager,
clock
)
resumeListener.setManager(mediaDataManager)
mediaDataManager.addListener(resumeListener)
@@ -396,12 +458,14 @@ class MediaResumeListenerTest : SysuiTestCase() {
// Then we store the new lastPlayed time
verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
.dropLastWhile { it.isEmpty() }.forEach {
val result = it.split("/")
assertThat(result.size).isEqualTo(3)
assertThat(result[2].toLong()).isEqualTo(currentTime)
}
componentCaptor.value
.split(ResumeMediaBrowser.DELIMITER.toRegex())
.dropLastWhile { it.isEmpty() }
.forEach {
val result = it.split("/")
assertThat(result.size).isEqualTo(3)
assertThat(result[2].toLong()).isEqualTo(currentTime)
}
verify(sharedPrefsEditor, times(1)).apply()
}
@@ -422,9 +486,7 @@ class MediaResumeListenerTest : SysuiTestCase() {
setUpMbsWithValidResolveInfo()
// Set up mocks to return with an error
whenever(resumeBrowser.testConnection()).thenAnswer {
callbackCaptor.value.onError()
}
whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
executor.runAllReady()

View File

@@ -37,8 +37,8 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
private const val PACKAGE_NAME = "package"
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 fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
private fun <T> any(): T = Mockito.any<T>()
@SmallTest
@@ -57,10 +59,8 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
private lateinit var resumeBrowser: TestableResumeMediaBrowser
private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
private val description = MediaDescription.Builder()
.setTitle(TITLE)
.setMediaId(MEDIA_ID)
.build()
private val description =
MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build()
@Mock lateinit var callback: ResumeMediaBrowser.Callback
@Mock lateinit var listener: MediaResumeListener
@@ -81,19 +81,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
whenever(browserFactory.create(any(), capture(connectionCallback), any()))
.thenReturn(browser)
.thenReturn(browser)
whenever(mediaController.transportControls).thenReturn(transportControls)
whenever(mediaController.sessionToken).thenReturn(token)
resumeBrowser = TestableResumeMediaBrowser(
context,
callback,
component,
browserFactory,
logger,
mediaController
)
resumeBrowser =
TestableResumeMediaBrowser(
context,
callback,
component,
browserFactory,
logger,
mediaController
)
}
@Test
@@ -329,30 +330,20 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
verify(oldBrowser).disconnect()
}
/**
* Helper function to mock a failed connection
*/
/** Helper function to mock a failed connection */
private fun setupBrowserFailed() {
whenever(browser.connect()).thenAnswer {
connectionCallback.value.onConnectionFailed()
}
whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() }
}
/**
* Helper function to mock a successful connection only
*/
/** Helper function to mock a successful connection only */
private fun setupBrowserConnection() {
whenever(browser.connect()).thenAnswer {
connectionCallback.value.onConnected()
}
whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() }
whenever(browser.isConnected()).thenReturn(true)
whenever(browser.getRoot()).thenReturn(ROOT)
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() {
setupBrowserConnection()
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() {
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() {
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(
context: Context,
callback: Callback,
@@ -403,4 +388,4 @@ public class ResumeMediaBrowserTest : SysuiTestCase() {
return fakeController
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -93,22 +93,23 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mediaCarouselController = MediaCarouselController(
context,
mediaControlPanelFactory,
visualStabilityProvider,
mediaHostStatesManager,
activityStarter,
clock,
executor,
mediaDataManager,
configurationController,
falsingCollector,
falsingManager,
dumpManager,
logger,
debugLogger
)
mediaCarouselController =
MediaCarouselController(
context,
mediaControlPanelFactory,
visualStabilityProvider,
mediaHostStatesManager,
activityStarter,
clock,
executor,
mediaDataManager,
configurationController,
falsingCollector,
falsingManager,
dumpManager,
logger,
debugLogger
)
verify(mediaDataManager).addListener(capture(listener))
verify(visualStabilityProvider)
.addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
@@ -121,60 +122,139 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test
fun testPlayerOrdering() {
// Test values: key, data, last active time
val playingLocal = Triple("playing local",
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
4500L)
val playingLocal =
Triple(
"playing local",
DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
),
4500L
)
val playingCast = Triple("playing cast",
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
5000L)
val playingCast =
Triple(
"playing cast",
DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
resumption = false
),
5000L
)
val pausedLocal = Triple("paused local",
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
1000L)
val pausedLocal =
Triple(
"paused local",
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
),
1000L
)
val pausedCast = Triple("paused cast",
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
2000L)
val pausedCast =
Triple(
"paused cast",
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
resumption = false
),
2000L
)
val playingRcn = Triple("playing RCN",
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
5000L)
val playingRcn =
Triple(
"playing RCN",
DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
resumption = false
),
5000L
)
val pausedRcn = Triple("paused RCN",
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
5000L)
val pausedRcn =
Triple(
"paused RCN",
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
resumption = false
),
5000L
)
val active = Triple("active",
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
250L)
val active =
Triple(
"active",
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
),
250L
)
val resume1 = Triple("resume 1",
DATA.copy(active = false, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
500L)
val resume1 =
Triple(
"resume 1",
DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
),
500L
)
val resume2 = Triple("resume 2",
DATA.copy(active = false, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
1000L)
val resume2 =
Triple(
"resume 2",
DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
),
1000L
)
val activeMoreRecent = Triple("active more recent",
DATA.copy(active = false, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L),
1000L)
val activeMoreRecent =
Triple(
"active more recent",
DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true,
lastActive = 2L
),
1000L
)
val activeLessRecent = Triple("active less recent",
DATA.copy(active = false, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L),
1000L)
val activeLessRecent =
Triple(
"active less recent",
DATA.copy(
active = false,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true,
lastActive = 1L
),
1000L
)
// Expected ordering for media players:
// Actively playing local sessions
// Actively playing cast sessions
@@ -182,13 +262,28 @@ class MediaCarouselControllerTest : SysuiTestCase() {
// RCNs
// Resume controls, by last active
val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn,
pausedRcn, active, resume2, resume1)
val expected =
listOf(
playingLocal,
playingCast,
pausedCast,
pausedLocal,
playingRcn,
pausedRcn,
active,
resume2,
resume1
)
expected.forEach {
clock.setCurrentTimeMillis(it.third)
MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first),
panel, clock, isSsReactivated = false)
MediaPlayerData.addMediaPlayer(
it.first,
it.second.copy(notificationKey = it.first),
panel,
clock,
isSsReactivated = false
)
}
for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
@@ -205,8 +300,13 @@ class MediaCarouselControllerTest : SysuiTestCase() {
testPlayerOrdering()
// If smartspace is prioritized
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
true, clock)
MediaPlayerData.addMediaRecommendation(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
panel,
true,
clock
)
// Then it should be shown immediately after any actively playing controls
assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
@@ -218,9 +318,9 @@ class MediaCarouselControllerTest : SysuiTestCase() {
// If smartspace is prioritized
listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
true
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
true
)
// Then it should be shown immediately after any actively playing controls
@@ -233,8 +333,13 @@ class MediaCarouselControllerTest : SysuiTestCase() {
testPlayerOrdering()
// If smartspace is not prioritized
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
false, clock)
MediaPlayerData.addMediaRecommendation(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
panel,
false,
clock
)
// Then it should be shown at the end of the carousel's active entries
val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
@@ -245,25 +350,36 @@ class MediaCarouselControllerTest : SysuiTestCase() {
fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
testPlayerOrdering()
// playing paused player
listener.value.onMediaDataLoaded("paused local",
"paused local",
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
listener.value.onMediaDataLoaded("playing local",
"playing local",
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(
"playing local",
"playing local",
DATA.copy(
active = true,
isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = true
)
)
assertEquals(
MediaPlayerData.getMediaPlayerIndex("paused local"),
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
MediaPlayerData.getMediaPlayerIndex("paused local"),
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
// paused player order should stays the same in visibleMediaPLayer map.
// paused player order should be first in mediaPlayer map.
assertEquals(
MediaPlayerData.visiblePlayerKeys().elementAt(3),
MediaPlayerData.playerKeys().elementAt(0)
MediaPlayerData.visiblePlayerKeys().elementAt(3),
MediaPlayerData.playerKeys().elementAt(0)
)
}
@Test
@@ -285,7 +401,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_QS,
mediaHostState,
animate = false)
animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
}
@@ -294,7 +411,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_QQS,
mediaHostState,
animate = false)
animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
}
@@ -303,7 +421,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_LOCKSCREEN,
mediaHostState,
animate = false)
animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
}
@@ -312,7 +431,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.onDesiredLocationChanged(
MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
mediaHostState,
animate = false)
animate = false
)
verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
}
@@ -321,10 +441,8 @@ class MediaCarouselControllerTest : SysuiTestCase() {
val packageName = "smartspace package"
val instanceId = InstanceId.fakeInstanceId(123)
val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
packageName = packageName,
instanceId = instanceId
)
val smartspaceData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId)
MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
mediaCarouselController.removePlayer(SMARTSPACE_KEY)
@@ -333,64 +451,99 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test
fun testMediaLoaded_ScrollToActivePlayer() {
listener.value.onMediaDataLoaded("playing local",
null,
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
listener.value.onMediaDataLoaded(
"playing local",
null,
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.
listener.value.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA,
false)
listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
false
)
mediaCarouselController.shouldScrollToKey = true
// switching between media players.
listener.value.onMediaDataLoaded("playing local",
"playing local",
DATA.copy(active = true, isPlaying = false,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true)
listener.value.onMediaDataLoaded(
"playing local",
"playing local",
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(
MediaPlayerData.getMediaPlayerIndex("paused local"),
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
MediaPlayerData.getMediaPlayerIndex("paused local"),
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
}
@Test
fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
false
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
false
)
listener.value.onMediaDataLoaded("playing local",
null,
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
listener.value.onMediaDataLoaded(
"playing local",
null,
DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false
)
)
var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
assertEquals(
playerIndex,
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
playerIndex,
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
assertEquals(playerIndex, 0)
// Replaying the same media player one more time.
// And check that the card stays in its position.
mediaCarouselController.shouldScrollToKey = true
listener.value.onMediaDataLoaded("playing local",
null,
DATA.copy(active = true, isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false,
packageName = "PACKAGE_NAME")
listener.value.onMediaDataLoaded(
"playing local",
null,
DATA.copy(
active = true,
isPlaying = true,
playbackLocation = MediaData.PLAYBACK_LOCAL,
resumption = false,
packageName = "PACKAGE_NAME"
)
)
playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
assertEquals(playerIndex, 0)
@@ -423,37 +576,43 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test
fun testGetCurrentVisibleMediaContentIntent() {
val clickIntent1 = mock(PendingIntent::class.java)
val player1 = Triple("player1",
DATA.copy(clickIntent = clickIntent1),
1000L)
val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L)
clock.setCurrentTimeMillis(player1.third)
MediaPlayerData.addMediaPlayer(player1.first,
player1.second.copy(notificationKey = player1.first),
panel, clock, isSsReactivated = false)
MediaPlayerData.addMediaPlayer(
player1.first,
player1.second.copy(notificationKey = player1.first),
panel,
clock,
isSsReactivated = false
)
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
val clickIntent2 = mock(PendingIntent::class.java)
val player2 = Triple("player2",
DATA.copy(clickIntent = clickIntent2),
2000L)
val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L)
clock.setCurrentTimeMillis(player2.third)
MediaPlayerData.addMediaPlayer(player2.first,
player2.second.copy(notificationKey = player2.first),
panel, clock, isSsReactivated = false)
MediaPlayerData.addMediaPlayer(
player2.first,
player2.second.copy(notificationKey = player2.first),
panel,
clock,
isSsReactivated = false
)
// mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
// added to the front because it was active more recently.
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
val clickIntent3 = mock(PendingIntent::class.java)
val player3 = Triple("player3",
DATA.copy(clickIntent = clickIntent3),
500L)
val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L)
clock.setCurrentTimeMillis(player3.third)
MediaPlayerData.addMediaPlayer(player3.first,
player3.second.copy(notificationKey = player3.first),
panel, clock, isSsReactivated = false)
MediaPlayerData.addMediaPlayer(
player3.first,
player3.second.copy(notificationKey = player3.first),
panel,
clock,
isSsReactivated = false
)
// mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
// added to the end because it was active less recently.
@@ -463,10 +622,14 @@ class MediaCarouselControllerTest : SysuiTestCase() {
@Test
fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
val delta = 0.0001F
val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation(
(PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION)
val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation(
(PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION)
val paginationSquishMiddle =
TRANSFORM_BEZIER.getInterpolation(
(PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
)
val paginationSquishEnd =
TRANSFORM_BEZIER.getInterpolation(
(PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION
)
whenever(mediaHostStatesManager.mediaHostStates)
.thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
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.broadcast.BroadcastSender
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.player.MediaAction
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.MediaViewHolder
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.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.util.MediaUiEventLogger
import com.android.systemui.media.dialog.MediaOutputDialogFactory
@@ -177,8 +177,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var session: MediaSession
private lateinit var device: MediaDeviceData
private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null,
showBroadcastButton = false)
private val disabledDevice =
MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false)
private lateinit var mediaData: MediaData
private val clock = FakeSystemClock()
@Mock private lateinit var logger: MediaUiEventLogger
@@ -225,24 +225,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
context.setMockPackageManager(packageManager)
player = object : MediaControlPanel(
context,
bgExecutor,
mainExecutor,
activityStarter,
broadcastSender,
mediaViewController,
seekBarViewModel,
Lazy { mediaDataManager },
mediaOutputDialogFactory,
mediaCarouselController,
falsingManager,
clock,
logger,
keyguardStateController,
activityIntentHelper,
lockscreenUserManager,
broadcastDialogController) {
player =
object :
MediaControlPanel(
context,
bgExecutor,
mainExecutor,
activityStarter,
broadcastSender,
mediaViewController,
seekBarViewModel,
Lazy { mediaDataManager },
mediaOutputDialogFactory,
mediaCarouselController,
falsingManager,
clock,
logger,
keyguardStateController,
activityIntentHelper,
lockscreenUserManager,
broadcastDialogController
) {
override fun loadAnimator(
animId: Int,
otionInterpolator: Interpolator,
@@ -263,18 +266,20 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Set valid recommendation data
val extras = Bundle()
extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME)
val intent = Intent().apply {
putExtras(extras)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val intent =
Intent().apply {
putExtras(extras)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
whenever(smartspaceAction.intent).thenReturn(intent)
whenever(smartspaceAction.extras).thenReturn(extras)
smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
packageName = PACKAGE,
instanceId = instanceId,
recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
cardAction = smartspaceAction
)
smartspaceData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
packageName = PACKAGE,
instanceId = instanceId,
recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
cardAction = smartspaceAction
)
}
private fun initGutsViewHolderMocks() {
@@ -292,36 +297,39 @@ public class MediaControlPanelTest : SysuiTestCase() {
}
private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
device = MediaDeviceData(true, null, name, null,
showBroadcastButton = shouldShowBroadcastButton)
device =
MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton)
// Create media session
val metadataBuilder = MediaMetadata.Builder().apply {
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)
setActions(PlaybackState.ACTION_PLAY)
}
session = MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
val metadataBuilder =
MediaMetadata.Builder().apply {
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)
setActions(PlaybackState.ACTION_PLAY)
}
session =
MediaSession(context, SESSION_KEY).apply {
setMetadata(metadataBuilder.build())
setPlaybackState(playbackBuilder.build())
}
session.setActive(true)
mediaData = MediaTestUtils.emptyMediaData.copy(
mediaData =
MediaTestUtils.emptyMediaData.copy(
artist = ARTIST,
song = TITLE,
packageName = PACKAGE,
token = session.sessionToken,
device = device,
instanceId = instanceId)
instanceId = instanceId
)
}
/**
* Initialize elements in media view holder
*/
/** Initialize elements in media view holder */
private fun initMediaViewHolderMocks() {
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
@@ -362,7 +370,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
action1.id,
action2.id,
action3.id,
action4.id)
action4.id
)
}
whenever(viewHolder.player).thenReturn(view)
@@ -407,9 +416,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
}
/**
* Initialize elements for the recommendation view holder
*/
/** Initialize elements for the recommendation view holder */
private fun initRecommendationViewHolderMocks() {
recTitle1 = TextView(context)
recTitle2 = TextView(context)
@@ -432,9 +439,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
.thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
whenever(recommendationViewHolder.mediaTitles)
.thenReturn(listOf(recTitle1, recTitle2, recTitle3))
whenever(recommendationViewHolder.mediaSubtitles).thenReturn(
listOf(recSubtitle1, recSubtitle2, recSubtitle3)
)
whenever(recommendationViewHolder.mediaSubtitles)
.thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
@@ -466,12 +472,13 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindSemanticActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
val semanticActions = MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", bg),
nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
custom0 = MediaAction(icon, null, "custom 0", bg),
custom1 = MediaAction(icon, null, "custom 1", bg)
)
val semanticActions =
MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", bg),
nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
custom0 = MediaAction(icon, null, "custom 0", bg),
custom1 = MediaAction(icon, null, "custom 1", bg)
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
@@ -514,15 +521,16 @@ public class MediaControlPanelTest : SysuiTestCase() {
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
// Setup button state: no prev or next button and their slots reserved
val semanticActions = MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", bg),
nextOrCustom = null,
prevOrCustom = null,
custom0 = MediaAction(icon, null, "custom 0", bg),
custom1 = MediaAction(icon, null, "custom 1", bg),
false,
true
)
val semanticActions =
MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", bg),
nextOrCustom = null,
prevOrCustom = null,
custom0 = MediaAction(icon, null, "custom 0", bg),
custom1 = MediaAction(icon, null, "custom 1", bg),
false,
true
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -543,15 +551,16 @@ public class MediaControlPanelTest : SysuiTestCase() {
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
// Setup button state: no prev or next button and their slots reserved
val semanticActions = MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", bg),
nextOrCustom = null,
prevOrCustom = null,
custom0 = MediaAction(icon, null, "custom 0", bg),
custom1 = MediaAction(icon, null, "custom 1", bg),
true,
false
)
val semanticActions =
MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", bg),
nextOrCustom = null,
prevOrCustom = null,
custom0 = MediaAction(icon, null, "custom 0", bg),
custom1 = MediaAction(icon, null, "custom 1", bg),
true,
false
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -659,10 +668,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
useRealConstraintSets()
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", null),
nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
)
val semanticActions =
MediaButton(
playOrPause = MediaAction(icon, Runnable {}, "play", null),
nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -732,9 +742,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
useRealConstraintSets()
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
)
val semanticActions =
MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null))
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -749,10 +758,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bind_notScrubbing_scrubbingViewsGone() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val semanticActions =
MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -783,10 +793,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
prevOrCustom = null,
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val semanticActions =
MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null))
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
@@ -803,10 +811,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = null
)
val semanticActions =
MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
@@ -823,10 +829,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val semanticActions =
MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
@@ -845,10 +852,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val semanticActions = MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val semanticActions =
MediaButton(
prevOrCustom = MediaAction(icon, {}, "prev", null),
nextOrCustom = MediaAction(icon, {}, "next", null)
)
val state = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -872,18 +880,20 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
val actions = listOf(
MediaAction(icon, Runnable {}, "previous", bg),
MediaAction(icon, Runnable {}, "play", bg),
MediaAction(icon, null, "next", bg),
MediaAction(icon, null, "custom 0", bg),
MediaAction(icon, Runnable {}, "custom 1", bg)
)
val state = mediaData.copy(
actions = actions,
actionsToShowInCompact = listOf(1, 2),
semanticActions = null
)
val actions =
listOf(
MediaAction(icon, Runnable {}, "previous", bg),
MediaAction(icon, Runnable {}, "play", bg),
MediaAction(icon, null, "next", bg),
MediaAction(icon, null, "custom 0", bg),
MediaAction(icon, Runnable {}, "custom 1", bg)
)
val state =
mediaData.copy(
actions = actions,
actionsToShowInCompact = listOf(1, 2),
semanticActions = null
)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
@@ -931,15 +941,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
val icon = context.getDrawable(R.drawable.ic_media_play)
val bg = context.getDrawable(R.drawable.ic_media_play_container)
val semanticActions0 = MediaButton(
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
)
val semanticActions1 = MediaButton(
playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)
)
val semanticActions2 = MediaButton(
playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)
)
val semanticActions0 =
MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
val semanticActions1 =
MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null))
val semanticActions2 =
MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null))
val state0 = mediaData.copy(semanticActions = semanticActions0)
val state1 = mediaData.copy(semanticActions = semanticActions1)
val state2 = mediaData.copy(semanticActions = semanticActions2)
@@ -1102,11 +1109,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
val semanticActions0 = MediaButton(
playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
)
val state = mediaData.copy(resumption = true, semanticActions = semanticActions0,
isPlaying = false)
val semanticActions0 =
MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
val state =
mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false)
player.attachPlayer(viewHolder)
player.bindPlayer(state, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
@@ -1445,9 +1451,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionPlayPauseClick_isLogged() {
val semanticActions = MediaButton(
playOrPause = MediaAction(null, Runnable {}, "play", null)
)
val semanticActions =
MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null))
val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -1459,9 +1464,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionPrevClick_isLogged() {
val semanticActions = MediaButton(
prevOrCustom = MediaAction(null, Runnable {}, "previous", null)
)
val semanticActions =
MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null))
val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -1473,9 +1477,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionNextClick_isLogged() {
val semanticActions = MediaButton(
nextOrCustom = MediaAction(null, Runnable {}, "next", null)
)
val semanticActions =
MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null))
val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -1487,9 +1490,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionCustom0Click_isLogged() {
val semanticActions = MediaButton(
custom0 = MediaAction(null, Runnable {}, "custom 0", null)
)
val semanticActions =
MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null))
val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -1501,9 +1503,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionCustom1Click_isLogged() {
val semanticActions = MediaButton(
custom1 = MediaAction(null, Runnable {}, "custom 1", null)
)
val semanticActions =
MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null))
val data = mediaData.copy(semanticActions = semanticActions)
player.attachPlayer(viewHolder)
@@ -1515,13 +1516,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionCustom2Click_isLogged() {
val actions = listOf(
MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 3", null),
MediaAction(null, Runnable {}, "action 4", null)
)
val actions =
listOf(
MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 3", null),
MediaAction(null, Runnable {}, "action 4", null)
)
val data = mediaData.copy(actions = actions)
player.attachPlayer(viewHolder)
@@ -1533,13 +1535,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionCustom3Click_isLogged() {
val actions = listOf(
MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 3", null),
MediaAction(null, Runnable {}, "action 4", null)
)
val actions =
listOf(
MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 3", null),
MediaAction(null, Runnable {}, "action 4", null)
)
val data = mediaData.copy(actions = actions)
player.attachPlayer(viewHolder)
@@ -1551,13 +1554,14 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun actionCustom4Click_isLogged() {
val actions = listOf(
MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 3", null),
MediaAction(null, Runnable {}, "action 4", null)
)
val actions =
listOf(
MediaAction(null, Runnable {}, "action 0", null),
MediaAction(null, Runnable {}, "action 1", null),
MediaAction(null, Runnable {}, "action 2", null),
MediaAction(null, Runnable {}, "action 3", null),
MediaAction(null, Runnable {}, "action 4", null)
)
val data = mediaData.copy(actions = actions)
player.attachPlayer(viewHolder)
@@ -1621,8 +1625,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
// THEN it shows without dismissing keyguard first
captor.value.onClick(viewHolder.player)
verify(activityStarter).startActivity(eq(clickIntent), eq(true),
nullable(), eq(true))
verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true))
}
@Test
@@ -1710,20 +1713,22 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_listHasTooFewRecs_notDisplayed() {
player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("subtitle1")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("subtitle2")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("subtitle1")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("subtitle2")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
)
)
)
player.bindRecommendation(data)
@@ -1735,30 +1740,32 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() {
player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("subtitle1")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("subtitle2")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "empty icon 1")
.setSubtitle("subtitle2")
.setIcon(null)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "empty icon 2")
.setSubtitle("subtitle2")
.setIcon(null)
.setExtras(Bundle.EMPTY)
.build(),
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("subtitle1")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("subtitle2")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "empty icon 1")
.setSubtitle("subtitle2")
.setIcon(null)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "empty icon 2")
.setSubtitle("subtitle2")
.setIcon(null)
.setExtras(Bundle.EMPTY)
.build(),
)
)
)
player.bindRecommendation(data)
@@ -1778,25 +1785,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
val subtitle3 = "Subtitle3"
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", title1)
.setSubtitle(subtitle1)
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", title2)
.setSubtitle(subtitle2)
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", title3)
.setSubtitle(subtitle3)
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", title1)
.setSubtitle(subtitle1)
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", title2)
.setSubtitle(subtitle2)
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", title3)
.setSubtitle(subtitle3)
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
)
)
)
player.bindRecommendation(data)
assertThat(recTitle1.text).isEqualTo(title1)
@@ -1811,15 +1820,17 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_noTitle_subtitleNotShown() {
player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("fake subtitle")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("fake subtitle")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
)
)
player.bindRecommendation(data)
assertThat(recSubtitle1.text).isEqualTo("")
@@ -1831,25 +1842,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("fake subtitle")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("fake subtitle")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "")
.setSubtitle("fake subtitle")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("fake subtitle")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("fake subtitle")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "")
.setSubtitle("fake subtitle")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
)
)
)
player.bindRecommendation(data)
assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
@@ -1863,25 +1876,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
player.attachRecommendation(recommendationViewHolder)
val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "title3")
.setSubtitle("subtitle3")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "title3")
.setSubtitle("subtitle3")
.setIcon(icon)
.setExtras(Bundle.EMPTY)
.build()
)
)
)
player.bindRecommendation(data)
assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
@@ -1893,25 +1908,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() {
useRealConstraintSets()
player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("")
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "title3")
.setSubtitle("")
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "title1")
.setSubtitle("")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "title2")
.setSubtitle("")
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "title3")
.setSubtitle("")
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
)
)
player.bindRecommendation(data)
@@ -1924,25 +1941,27 @@ public class MediaControlPanelTest : SysuiTestCase() {
fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() {
useRealConstraintSets()
player.attachRecommendation(recommendationViewHolder)
val data = smartspaceData.copy(
recommendations = listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("subtitle1")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "")
.setSubtitle("subtitle2")
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "")
.setSubtitle("subtitle3")
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
val data =
smartspaceData.copy(
recommendations =
listOf(
SmartspaceAction.Builder("id1", "")
.setSubtitle("subtitle1")
.setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id2", "")
.setSubtitle("subtitle2")
.setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
.setExtras(Bundle.EMPTY)
.build(),
SmartspaceAction.Builder("id3", "")
.setSubtitle("subtitle3")
.setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
.setExtras(Bundle.EMPTY)
.build()
)
)
)
player.bindRecommendation(data)
@@ -1955,20 +1974,23 @@ public class MediaControlPanelTest : SysuiTestCase() {
}
private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
withArgCaptor {
verify(seekBarViewModel).setScrubbingChangeListener(capture())
}
private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener =
withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) }
private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor {
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
* visibilities. If we use mocks for the ConstraintSets, then action visibility changes are
* just thrown away instead of being saved for reference later. This method sets us up to use
* ConstraintSets so that we do save visibility changes.
* 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 just
* thrown away instead of being saved for reference later. This method sets us up to use
* 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() {
expandedSet = ConstraintSet()

View File

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

View File

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

View File

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

View File

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