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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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?
|
||||
) {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,4 +73,4 @@ internal open class MetadataAnimationHandler(
|
||||
exitAnimator.addListener(this)
|
||||
enterAnimator.addListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user