Merge "Add seek bar to QS Media Player" into rvc-dev am: b131e1b746 am: 83d91902a7 am: 91b1376efb
Change-Id: Idc638c59cfc1a10f576a65aa03acd1df8cb30685
This commit is contained in:
@@ -136,6 +136,47 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Seek Bar -->
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/media_progress_bar"
|
||||||
|
android:clickable="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxHeight="3dp"
|
||||||
|
android:paddingTop="24dp"
|
||||||
|
android:paddingBottom="24dp"
|
||||||
|
android:layout_marginBottom="-24dp"
|
||||||
|
android:layout_marginTop="-24dp"
|
||||||
|
android:splitTrack="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/notification_media_progress_time"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
>
|
||||||
|
<!-- width is set to "match_parent" to avoid extra layout calls -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/media_elapsed_time"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:fontFamily="@*android:string/config_bodyFontFamily"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:gravity="left"
|
||||||
|
/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/media_total_time"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@*android:string/config_bodyFontFamily"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:gravity="right"
|
||||||
|
/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/media_actions"
|
android:id="@+id/media_actions"
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.UiThread
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
|
||||||
|
import com.android.systemui.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer for changes from SeekBarViewModel.
|
||||||
|
*
|
||||||
|
* <p>Updates the seek bar views in response to changes to the model.
|
||||||
|
*/
|
||||||
|
class SeekBarObserver(view: View) : Observer<SeekBarViewModel.Progress> {
|
||||||
|
|
||||||
|
private val seekBarView: SeekBar
|
||||||
|
private val elapsedTimeView: TextView
|
||||||
|
private val totalTimeView: TextView
|
||||||
|
|
||||||
|
init {
|
||||||
|
seekBarView = view.findViewById(R.id.media_progress_bar)
|
||||||
|
elapsedTimeView = view.findViewById(R.id.media_elapsed_time)
|
||||||
|
totalTimeView = view.findViewById(R.id.media_total_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates seek bar views when the data model changes. */
|
||||||
|
@UiThread
|
||||||
|
override fun onChanged(data: SeekBarViewModel.Progress) {
|
||||||
|
if (data.enabled && seekBarView.visibility == View.GONE) {
|
||||||
|
seekBarView.visibility = View.VISIBLE
|
||||||
|
elapsedTimeView.visibility = View.VISIBLE
|
||||||
|
totalTimeView.visibility = View.VISIBLE
|
||||||
|
} else if (!data.enabled && seekBarView.visibility == View.VISIBLE) {
|
||||||
|
seekBarView.visibility = View.GONE
|
||||||
|
elapsedTimeView.visibility = View.GONE
|
||||||
|
totalTimeView.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update the style of the disabled progress bar
|
||||||
|
seekBarView.setEnabled(data.seekAvailable)
|
||||||
|
|
||||||
|
data.color?.let {
|
||||||
|
var tintList = ColorStateList.valueOf(it)
|
||||||
|
seekBarView.setThumbTintList(tintList)
|
||||||
|
tintList = tintList.withAlpha(192) // 75%
|
||||||
|
seekBarView.setProgressTintList(tintList)
|
||||||
|
tintList = tintList.withAlpha(128) // 50%
|
||||||
|
seekBarView.setProgressBackgroundTintList(tintList)
|
||||||
|
elapsedTimeView.setTextColor(it)
|
||||||
|
totalTimeView.setTextColor(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.elapsedTime?.let {
|
||||||
|
seekBarView.setProgress(it)
|
||||||
|
elapsedTimeView.setText(DateUtils.formatElapsedTime(
|
||||||
|
it / DateUtils.SECOND_IN_MILLIS))
|
||||||
|
}
|
||||||
|
|
||||||
|
data.duration?.let {
|
||||||
|
seekBarView.setMax(it)
|
||||||
|
totalTimeView.setText(DateUtils.formatElapsedTime(
|
||||||
|
it / DateUtils.SECOND_IN_MILLIS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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
|
||||||
|
|
||||||
|
import android.media.MediaMetadata
|
||||||
|
import android.media.session.MediaController
|
||||||
|
import android.media.session.PlaybackState
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import androidx.annotation.AnyThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
|
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||||
|
|
||||||
|
private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
|
||||||
|
|
||||||
|
/** ViewModel for seek bar in QS media player. */
|
||||||
|
class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
|
||||||
|
|
||||||
|
private val _progress = MutableLiveData<Progress>().apply {
|
||||||
|
postValue(Progress(false, false, null, null, null))
|
||||||
|
}
|
||||||
|
val progress: LiveData<Progress>
|
||||||
|
get() = _progress
|
||||||
|
private var controller: MediaController? = null
|
||||||
|
private var playbackState: PlaybackState? = null
|
||||||
|
|
||||||
|
/** Listening state (QS open or closed) is used to control polling of progress. */
|
||||||
|
var listening = true
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
checkPlaybackPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle request to change the current position in the media track.
|
||||||
|
* @param position Place to seek to in the track.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun onSeek(position: Long) {
|
||||||
|
controller?.transportControls?.seekTo(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates media information.
|
||||||
|
* @param mediaController controller for media session
|
||||||
|
* @param color foreground color for UI elements
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun updateController(mediaController: MediaController?, color: Int) {
|
||||||
|
controller = mediaController
|
||||||
|
playbackState = controller?.playbackState
|
||||||
|
val mediaMetadata = controller?.metadata
|
||||||
|
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()
|
||||||
|
val enabled = if (duration != null && duration <= 0) false else true
|
||||||
|
_progress.postValue(Progress(enabled, seekAvailable, position, duration, color))
|
||||||
|
if (shouldPollPlaybackPosition()) {
|
||||||
|
checkPlaybackPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private fun checkPlaybackPosition(): Runnable = bgExecutor.executeDelayed({
|
||||||
|
val currentPosition = controller?.playbackState?.position?.toInt()
|
||||||
|
if (currentPosition != null && _progress.value!!.elapsedTime != currentPosition) {
|
||||||
|
_progress.postValue(_progress.value!!.copy(elapsedTime = currentPosition))
|
||||||
|
}
|
||||||
|
if (shouldPollPlaybackPosition()) {
|
||||||
|
checkPlaybackPosition()
|
||||||
|
}
|
||||||
|
}, POSITION_UPDATE_INTERVAL_MILLIS)
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun shouldPollPlaybackPosition(): Boolean {
|
||||||
|
val state = playbackState?.state
|
||||||
|
val moving = if (state == null) false else
|
||||||
|
state == PlaybackState.STATE_PLAYING ||
|
||||||
|
state == PlaybackState.STATE_BUFFERING ||
|
||||||
|
state == PlaybackState.STATE_FAST_FORWARDING ||
|
||||||
|
state == PlaybackState.STATE_REWINDING
|
||||||
|
return moving && listening
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets a listener to attach to the seek bar to handle seeking. */
|
||||||
|
val seekBarListener: SeekBar.OnSeekBarChangeListener
|
||||||
|
get() {
|
||||||
|
return SeekBarChangeListener(this, bgExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets a listener to attach to the seek bar to disable touch intercepting. */
|
||||||
|
val seekBarTouchListener: View.OnTouchListener
|
||||||
|
get() {
|
||||||
|
return SeekBarTouchListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SeekBarChangeListener(
|
||||||
|
val viewModel: SeekBarViewModel,
|
||||||
|
val bgExecutor: DelayableExecutor
|
||||||
|
) : SeekBar.OnSeekBarChangeListener {
|
||||||
|
override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
|
if (fromUser) {
|
||||||
|
bgExecutor.execute {
|
||||||
|
viewModel.onSeek(progress.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onStartTrackingTouch(bar: SeekBar) {
|
||||||
|
}
|
||||||
|
override fun onStopTrackingTouch(bar: SeekBar) {
|
||||||
|
val pos = bar.progress.toLong()
|
||||||
|
bgExecutor.execute {
|
||||||
|
viewModel.onSeek(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SeekBarTouchListener : View.OnTouchListener {
|
||||||
|
override fun onTouch(view: View, event: MotionEvent): Boolean {
|
||||||
|
view.parent.requestDisallowInterceptTouchEvent(true)
|
||||||
|
return view.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** State seen by seek bar UI. */
|
||||||
|
data class Progress(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val seekAvailable: Boolean,
|
||||||
|
val elapsedTime: Int?,
|
||||||
|
val duration: Int?,
|
||||||
|
val color: Int?
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
|
|
||||||
package com.android.systemui.qs;
|
package com.android.systemui.qs;
|
||||||
|
|
||||||
|
import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.Icon;
|
import android.graphics.drawable.Icon;
|
||||||
|
import android.media.session.MediaController;
|
||||||
import android.media.session.MediaSession;
|
import android.media.session.MediaSession;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -28,12 +31,16 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.android.settingslib.media.MediaDevice;
|
import com.android.settingslib.media.MediaDevice;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.media.MediaControlPanel;
|
import com.android.systemui.media.MediaControlPanel;
|
||||||
|
import com.android.systemui.media.SeekBarObserver;
|
||||||
|
import com.android.systemui.media.SeekBarViewModel;
|
||||||
import com.android.systemui.statusbar.NotificationMediaManager;
|
import com.android.systemui.statusbar.NotificationMediaManager;
|
||||||
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
@@ -54,6 +61,9 @@ public class QSMediaPlayer extends MediaControlPanel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private final QSPanel mParent;
|
private final QSPanel mParent;
|
||||||
|
private final DelayableExecutor mBackgroundExecutor;
|
||||||
|
private final SeekBarViewModel mSeekBarViewModel;
|
||||||
|
private final SeekBarObserver mSeekBarObserver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize quick shade version of player
|
* Initialize quick shade version of player
|
||||||
@@ -64,10 +74,20 @@ public class QSMediaPlayer extends MediaControlPanel {
|
|||||||
* @param backgroundExecutor
|
* @param backgroundExecutor
|
||||||
*/
|
*/
|
||||||
public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
|
public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
|
||||||
Executor foregroundExecutor, Executor backgroundExecutor) {
|
Executor foregroundExecutor, DelayableExecutor backgroundExecutor) {
|
||||||
super(context, parent, manager, R.layout.qs_media_panel, QS_ACTION_IDS, foregroundExecutor,
|
super(context, parent, manager, R.layout.qs_media_panel, QS_ACTION_IDS, foregroundExecutor,
|
||||||
backgroundExecutor);
|
backgroundExecutor);
|
||||||
mParent = (QSPanel) parent;
|
mParent = (QSPanel) parent;
|
||||||
|
mBackgroundExecutor = backgroundExecutor;
|
||||||
|
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
|
||||||
|
mSeekBarObserver = new SeekBarObserver(getView());
|
||||||
|
// Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
|
||||||
|
// priority of players. As soon as it is removed, the lifecycle will end and the seek bar
|
||||||
|
// will stop updating. So, use the lifecycle of the parent instead.
|
||||||
|
mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
|
||||||
|
SeekBar bar = getView().findViewById(R.id.media_progress_bar);
|
||||||
|
bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
|
||||||
|
bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,6 +135,11 @@ public class QSMediaPlayer extends MediaControlPanel {
|
|||||||
thisBtn.setVisibility(View.GONE);
|
thisBtn.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seek Bar
|
||||||
|
final MediaController controller = new MediaController(getContext(), token);
|
||||||
|
mBackgroundExecutor.execute(
|
||||||
|
() -> mSeekBarViewModel.updateController(controller, iconColor));
|
||||||
|
|
||||||
// Set up long press menu
|
// Set up long press menu
|
||||||
View guts = mMediaNotifView.findViewById(R.id.media_guts);
|
View guts = mMediaNotifView.findViewById(R.id.media_guts);
|
||||||
View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
|
View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
|
||||||
@@ -155,4 +180,16 @@ public class QSMediaPlayer extends MediaControlPanel {
|
|||||||
return true; // consumed click
|
return true; // consumed click
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the listening state of the player.
|
||||||
|
*
|
||||||
|
* Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
|
||||||
|
* unnecessary work when the QS panel is closed.
|
||||||
|
*
|
||||||
|
* @param listening True when player should be active. Otherwise, false.
|
||||||
|
*/
|
||||||
|
public void setListening(boolean listening) {
|
||||||
|
mSeekBarViewModel.setListening(listening);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController;
|
|||||||
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
|
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
|
||||||
import com.android.systemui.tuner.TunerService;
|
import com.android.systemui.tuner.TunerService;
|
||||||
import com.android.systemui.tuner.TunerService.Tunable;
|
import com.android.systemui.tuner.TunerService.Tunable;
|
||||||
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
@@ -103,7 +104,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
private final NotificationMediaManager mNotificationMediaManager;
|
private final NotificationMediaManager mNotificationMediaManager;
|
||||||
private final LocalBluetoothManager mLocalBluetoothManager;
|
private final LocalBluetoothManager mLocalBluetoothManager;
|
||||||
private final Executor mForegroundExecutor;
|
private final Executor mForegroundExecutor;
|
||||||
private final Executor mBackgroundExecutor;
|
private final DelayableExecutor mBackgroundExecutor;
|
||||||
private LocalMediaManager mLocalMediaManager;
|
private LocalMediaManager mLocalMediaManager;
|
||||||
private MediaDevice mDevice;
|
private MediaDevice mDevice;
|
||||||
private boolean mUpdateCarousel = false;
|
private boolean mUpdateCarousel = false;
|
||||||
@@ -166,7 +167,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
QSLogger qsLogger,
|
QSLogger qsLogger,
|
||||||
NotificationMediaManager notificationMediaManager,
|
NotificationMediaManager notificationMediaManager,
|
||||||
@Main Executor foregroundExecutor,
|
@Main Executor foregroundExecutor,
|
||||||
@Background Executor backgroundExecutor,
|
@Background DelayableExecutor backgroundExecutor,
|
||||||
@Nullable LocalBluetoothManager localBluetoothManager
|
@Nullable LocalBluetoothManager localBluetoothManager
|
||||||
) {
|
) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
@@ -278,7 +279,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
Log.d(TAG, "creating new player");
|
Log.d(TAG, "creating new player");
|
||||||
player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
|
player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
|
||||||
mForegroundExecutor, mBackgroundExecutor);
|
mForegroundExecutor, mBackgroundExecutor);
|
||||||
|
player.setListening(mListening);
|
||||||
if (player.isPlaying()) {
|
if (player.isPlaying()) {
|
||||||
mMediaCarousel.addView(player.getView(), 0, lp); // add in front
|
mMediaCarousel.addView(player.getView(), 0, lp); // add in front
|
||||||
} else {
|
} else {
|
||||||
@@ -584,6 +585,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
if (mListening) {
|
if (mListening) {
|
||||||
refreshAllTiles();
|
refreshAllTiles();
|
||||||
}
|
}
|
||||||
|
for (QSMediaPlayer player : mMediaPlayers) {
|
||||||
|
player.setListening(mListening);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getTilesSpecs() {
|
private String getTilesSpecs() {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import com.android.systemui.statusbar.NotificationMediaManager;
|
|||||||
import com.android.systemui.tuner.TunerService;
|
import com.android.systemui.tuner.TunerService;
|
||||||
import com.android.systemui.tuner.TunerService.Tunable;
|
import com.android.systemui.tuner.TunerService.Tunable;
|
||||||
import com.android.systemui.util.Utils;
|
import com.android.systemui.util.Utils;
|
||||||
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -81,7 +82,7 @@ public class QuickQSPanel extends QSPanel {
|
|||||||
QSLogger qsLogger,
|
QSLogger qsLogger,
|
||||||
NotificationMediaManager notificationMediaManager,
|
NotificationMediaManager notificationMediaManager,
|
||||||
@Main Executor foregroundExecutor,
|
@Main Executor foregroundExecutor,
|
||||||
@Background Executor backgroundExecutor,
|
@Background DelayableExecutor backgroundExecutor,
|
||||||
@Nullable LocalBluetoothManager localBluetoothManager
|
@Nullable LocalBluetoothManager localBluetoothManager
|
||||||
) {
|
) {
|
||||||
super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager,
|
super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager,
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public class SysuiLifecycle {
|
|||||||
|
|
||||||
ViewLifecycle(View v) {
|
ViewLifecycle(View v) {
|
||||||
v.addOnAttachStateChangeListener(this);
|
v.addOnAttachStateChangeListener(this);
|
||||||
|
if (v.isAttachedToWindow()) {
|
||||||
|
mLifecycle.markState(RESUMED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.testing.TestableLooper
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
@TestableLooper.RunWithLooper
|
||||||
|
public class SeekBarObserverTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
private lateinit var observer: SeekBarObserver
|
||||||
|
@Mock private lateinit var mockView: View
|
||||||
|
private lateinit var seekBarView: SeekBar
|
||||||
|
private lateinit var elapsedTimeView: TextView
|
||||||
|
private lateinit var totalTimeView: TextView
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockView = mock(View::class.java)
|
||||||
|
seekBarView = SeekBar(context)
|
||||||
|
elapsedTimeView = TextView(context)
|
||||||
|
totalTimeView = TextView(context)
|
||||||
|
whenever<SeekBar>(
|
||||||
|
mockView.findViewById(R.id.media_progress_bar)).thenReturn(seekBarView)
|
||||||
|
whenever<TextView>(
|
||||||
|
mockView.findViewById(R.id.media_elapsed_time)).thenReturn(elapsedTimeView)
|
||||||
|
whenever<TextView>(mockView.findViewById(R.id.media_total_time)).thenReturn(totalTimeView)
|
||||||
|
observer = SeekBarObserver(mockView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seekBarGone() {
|
||||||
|
// WHEN seek bar is disabled
|
||||||
|
val isEnabled = false
|
||||||
|
val data = SeekBarViewModel.Progress(isEnabled, false, null, null, null)
|
||||||
|
observer.onChanged(data)
|
||||||
|
// THEN seek bar visibility is set to GONE
|
||||||
|
assertThat(seekBarView.getVisibility()).isEqualTo(View.GONE)
|
||||||
|
assertThat(elapsedTimeView.getVisibility()).isEqualTo(View.GONE)
|
||||||
|
assertThat(totalTimeView.getVisibility()).isEqualTo(View.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seekBarVisible() {
|
||||||
|
// WHEN seek bar is enabled
|
||||||
|
val isEnabled = true
|
||||||
|
val data = SeekBarViewModel.Progress(isEnabled, true, 3000, 12000, -1)
|
||||||
|
observer.onChanged(data)
|
||||||
|
// THEN seek bar is visible
|
||||||
|
assertThat(seekBarView.getVisibility()).isEqualTo(View.VISIBLE)
|
||||||
|
assertThat(elapsedTimeView.getVisibility()).isEqualTo(View.VISIBLE)
|
||||||
|
assertThat(totalTimeView.getVisibility()).isEqualTo(View.VISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seekBarProgress() {
|
||||||
|
// WHEN seek bar progress is about half
|
||||||
|
val data = SeekBarViewModel.Progress(true, true, 3000, 120000, -1)
|
||||||
|
observer.onChanged(data)
|
||||||
|
// THEN seek bar is visible
|
||||||
|
assertThat(seekBarView.progress).isEqualTo(100)
|
||||||
|
assertThat(seekBarView.max).isEqualTo(120000)
|
||||||
|
assertThat(elapsedTimeView.getText()).isEqualTo("00:03")
|
||||||
|
assertThat(totalTimeView.getText()).isEqualTo("02:00")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seekBarDisabledWhenSeekNotAvailable() {
|
||||||
|
// WHEN seek is not available
|
||||||
|
val isSeekAvailable = false
|
||||||
|
val data = SeekBarViewModel.Progress(true, isSeekAvailable, 3000, 120000, -1)
|
||||||
|
observer.onChanged(data)
|
||||||
|
// THEN seek bar is not enabled
|
||||||
|
assertThat(seekBarView.isEnabled()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seekBarEnabledWhenSeekNotAvailable() {
|
||||||
|
// WHEN seek is available
|
||||||
|
val isSeekAvailable = true
|
||||||
|
val data = SeekBarViewModel.Progress(true, isSeekAvailable, 3000, 120000, -1)
|
||||||
|
observer.onChanged(data)
|
||||||
|
// THEN seek bar is not enabled
|
||||||
|
assertThat(seekBarView.isEnabled()).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seekBarColor() {
|
||||||
|
// WHEN data included color
|
||||||
|
val data = SeekBarViewModel.Progress(true, true, 3000, 120000, Color.RED)
|
||||||
|
observer.onChanged(data)
|
||||||
|
// THEN seek bar is colored
|
||||||
|
val red = ColorStateList.valueOf(Color.RED)
|
||||||
|
assertThat(elapsedTimeView.getTextColors()).isEqualTo(red)
|
||||||
|
assertThat(totalTimeView.getTextColors()).isEqualTo(red)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.media.MediaMetadata
|
||||||
|
import android.media.session.MediaController
|
||||||
|
import android.media.session.PlaybackState
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.testing.TestableLooper
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import androidx.arch.core.executor.ArchTaskExecutor
|
||||||
|
import androidx.arch.core.executor.TaskExecutor
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
@TestableLooper.RunWithLooper
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
fakeExecutor = FakeExecutor(FakeSystemClock())
|
||||||
|
viewModel = SeekBarViewModel(fakeExecutor)
|
||||||
|
mockController = mock(MediaController::class.java)
|
||||||
|
mockTransport = mock(MediaController.TransportControls::class.java)
|
||||||
|
|
||||||
|
// LiveData to run synchronously
|
||||||
|
ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
ArchTaskExecutor.getInstance().setDelegate(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateColor() {
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
assertThat(viewModel.progress.value!!.color).isEqualTo(Color.RED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateDuration() {
|
||||||
|
// GIVEN that the duration is contained within the metadata
|
||||||
|
val duration = 12000L
|
||||||
|
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, Color.RED)
|
||||||
|
// THEN the duration is extracted
|
||||||
|
assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
|
||||||
|
assertThat(viewModel.progress.value!!.enabled).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateDurationNegative() {
|
||||||
|
// GIVEN that the duration is negative
|
||||||
|
val duration = -1L
|
||||||
|
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, Color.RED)
|
||||||
|
// THEN the seek bar is disabled
|
||||||
|
assertThat(viewModel.progress.value!!.enabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateDurationZero() {
|
||||||
|
// GIVEN that the duration is zero
|
||||||
|
val duration = 0L
|
||||||
|
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, Color.RED)
|
||||||
|
// THEN the seek bar is disabled
|
||||||
|
assertThat(viewModel.progress.value!!.enabled).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateElapsedTime() {
|
||||||
|
// GIVEN that the PlaybackState contins the current position
|
||||||
|
val position = 200L
|
||||||
|
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, Color.RED)
|
||||||
|
// THEN elapsed time is captured
|
||||||
|
assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(200.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateSeekAvailable() {
|
||||||
|
// GIVEN that seek is included in actions
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setActions(PlaybackState.ACTION_SEEK_TO)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
// WHEN the controller is updated
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// THEN seek is available
|
||||||
|
assertThat(viewModel.progress.value!!.seekAvailable).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateSeekNotAvailable() {
|
||||||
|
// GIVEN that seek is not included in actions
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setActions(PlaybackState.ACTION_PLAY)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
// WHEN the controller is updated
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// THEN seek is not available
|
||||||
|
assertThat(viewModel.progress.value!!.seekAvailable).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSeek() {
|
||||||
|
whenever(mockController.getTransportControls()).thenReturn(mockTransport)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN user input is dispatched
|
||||||
|
val pos = 42L
|
||||||
|
viewModel.onSeek(pos)
|
||||||
|
fakeExecutor.runAllReady()
|
||||||
|
// THEN transport controls should be used
|
||||||
|
verify(mockTransport).seekTo(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleProgressChangedUser() {
|
||||||
|
whenever(mockController.getTransportControls()).thenReturn(mockTransport)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN user starts dragging the seek bar
|
||||||
|
val pos = 42
|
||||||
|
viewModel.seekBarListener.onProgressChanged(SeekBar(context), pos, true)
|
||||||
|
fakeExecutor.runAllReady()
|
||||||
|
// THEN transport controls should be used
|
||||||
|
verify(mockTransport).seekTo(pos.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleProgressChangedOther() {
|
||||||
|
whenever(mockController.getTransportControls()).thenReturn(mockTransport)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN user starts dragging the seek bar
|
||||||
|
val pos = 42
|
||||||
|
viewModel.seekBarListener.onProgressChanged(SeekBar(context), pos, false)
|
||||||
|
fakeExecutor.runAllReady()
|
||||||
|
// THEN transport controls should be used
|
||||||
|
verify(mockTransport, never()).seekTo(pos.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleStartTrackingTouch() {
|
||||||
|
whenever(mockController.getTransportControls()).thenReturn(mockTransport)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN user starts dragging the seek bar
|
||||||
|
val pos = 42
|
||||||
|
val bar = SeekBar(context).apply {
|
||||||
|
progress = pos
|
||||||
|
}
|
||||||
|
viewModel.seekBarListener.onStartTrackingTouch(bar)
|
||||||
|
fakeExecutor.runAllReady()
|
||||||
|
// THEN transport controls should be used
|
||||||
|
verify(mockTransport, never()).seekTo(pos.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleStopTrackingTouch() {
|
||||||
|
whenever(mockController.getTransportControls()).thenReturn(mockTransport)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN user ends drag
|
||||||
|
val pos = 42
|
||||||
|
val bar = SeekBar(context).apply {
|
||||||
|
progress = pos
|
||||||
|
}
|
||||||
|
viewModel.seekBarListener.onStopTrackingTouch(bar)
|
||||||
|
fakeExecutor.runAllReady()
|
||||||
|
// THEN transport controls should be used
|
||||||
|
verify(mockTransport).seekTo(pos.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun queuePollTaskWhenPlaying() {
|
||||||
|
// GIVEN that the track is playing
|
||||||
|
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, Color.RED)
|
||||||
|
// THEN a task is queued
|
||||||
|
assertThat(fakeExecutor.numPending()).isEqualTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun noQueuePollTaskWhenStopped() {
|
||||||
|
// GIVEN that the playback state is stopped
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
// WHEN updated
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// THEN an update task is not queued
|
||||||
|
assertThat(fakeExecutor.numPending()).isEqualTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun queuePollTaskWhenListening() {
|
||||||
|
// GIVEN listening
|
||||||
|
viewModel.listening = true
|
||||||
|
with(fakeExecutor) {
|
||||||
|
advanceClockToNext()
|
||||||
|
runAllReady()
|
||||||
|
}
|
||||||
|
// AND the playback state is playing
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_PLAYING, 200L, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
// WHEN updated
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// THEN an update task is queued
|
||||||
|
assertThat(fakeExecutor.numPending()).isEqualTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun noQueuePollTaskWhenNotListening() {
|
||||||
|
// GIVEN not listening
|
||||||
|
viewModel.listening = false
|
||||||
|
with(fakeExecutor) {
|
||||||
|
advanceClockToNext()
|
||||||
|
runAllReady()
|
||||||
|
}
|
||||||
|
// AND the playback state is playing
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
// WHEN updated
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// THEN an update task is not queued
|
||||||
|
assertThat(fakeExecutor.numPending()).isEqualTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
|
||||||
|
// GIVEN that the track is playing
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_PLAYING, 100L, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN the next task runs
|
||||||
|
with(fakeExecutor) {
|
||||||
|
advanceClockToNext()
|
||||||
|
runAllReady()
|
||||||
|
}
|
||||||
|
// THEN another task is queued
|
||||||
|
assertThat(fakeExecutor.numPending()).isEqualTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun taskUpdatesProgress() {
|
||||||
|
// GIVEN that the PlaybackState contins the current position
|
||||||
|
val position = 200L
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_PLAYING, position, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// AND the playback state advances
|
||||||
|
val nextPosition = 300L
|
||||||
|
val nextState = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_PLAYING, nextPosition, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(nextState)
|
||||||
|
// WHEN the task runs
|
||||||
|
with(fakeExecutor) {
|
||||||
|
advanceClockToNext()
|
||||||
|
runAllReady()
|
||||||
|
}
|
||||||
|
// THEN elapsed time is captured
|
||||||
|
assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(nextPosition.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun startListeningQueuesPollTask() {
|
||||||
|
// GIVEN not listening
|
||||||
|
viewModel.listening = false
|
||||||
|
with(fakeExecutor) {
|
||||||
|
advanceClockToNext()
|
||||||
|
runAllReady()
|
||||||
|
}
|
||||||
|
// AND the playback state is playing
|
||||||
|
val state = PlaybackState.Builder().run {
|
||||||
|
setState(PlaybackState.STATE_STOPPED, 200L, 1f)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
whenever(mockController.getPlaybackState()).thenReturn(state)
|
||||||
|
viewModel.updateController(mockController, Color.RED)
|
||||||
|
// WHEN start listening
|
||||||
|
viewModel.listening = true
|
||||||
|
// THEN an update task is queued
|
||||||
|
assertThat(fakeExecutor.numPending()).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import com.android.systemui.qs.customize.QSCustomizer;
|
|||||||
import com.android.systemui.qs.logging.QSLogger;
|
import com.android.systemui.qs.logging.QSLogger;
|
||||||
import com.android.systemui.qs.tileimpl.QSTileImpl;
|
import com.android.systemui.qs.tileimpl.QSTileImpl;
|
||||||
import com.android.systemui.statusbar.NotificationMediaManager;
|
import com.android.systemui.statusbar.NotificationMediaManager;
|
||||||
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -87,7 +88,7 @@ public class QSPanelTest extends SysuiTestCase {
|
|||||||
@Mock
|
@Mock
|
||||||
private Executor mForegroundExecutor;
|
private Executor mForegroundExecutor;
|
||||||
@Mock
|
@Mock
|
||||||
private Executor mBackgroundExecutor;
|
private DelayableExecutor mBackgroundExecutor;
|
||||||
@Mock
|
@Mock
|
||||||
private LocalBluetoothManager mLocalBluetoothManager;
|
private LocalBluetoothManager mLocalBluetoothManager;
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
|
|||||||
|
|
||||||
import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
|
import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -35,12 +37,15 @@ import android.testing.TestableLooper.RunWithLooper;
|
|||||||
import android.testing.ViewUtils;
|
import android.testing.ViewUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.lifecycle.LifecycleEventObserver;
|
import androidx.lifecycle.LifecycleEventObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.test.filters.SmallTest;
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@@ -49,39 +54,122 @@ import org.junit.runner.RunWith;
|
|||||||
@SmallTest
|
@SmallTest
|
||||||
public class SysuiLifecycleTest extends SysuiTestCase {
|
public class SysuiLifecycleTest extends SysuiTestCase {
|
||||||
|
|
||||||
|
private View mView;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mView = new View(mContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
if (mView.isAttachedToWindow()) {
|
||||||
|
ViewUtils.detachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAttach() {
|
public void testAttach() {
|
||||||
View v = new View(mContext);
|
|
||||||
LifecycleEventObserver observer = mock(LifecycleEventObserver.class);
|
LifecycleEventObserver observer = mock(LifecycleEventObserver.class);
|
||||||
LifecycleOwner lifecycle = viewAttachLifecycle(v);
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
lifecycle.getLifecycle().addObserver(observer);
|
lifecycle.getLifecycle().addObserver(observer);
|
||||||
|
|
||||||
ViewUtils.attachView(v);
|
ViewUtils.attachView(mView);
|
||||||
TestableLooper.get(this).processAllMessages();
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
|
||||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_CREATE));
|
verify(observer).onStateChanged(eq(lifecycle), eq(ON_CREATE));
|
||||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_START));
|
verify(observer).onStateChanged(eq(lifecycle), eq(ON_START));
|
||||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_RESUME));
|
verify(observer).onStateChanged(eq(lifecycle), eq(ON_RESUME));
|
||||||
|
|
||||||
ViewUtils.detachView(v);
|
|
||||||
TestableLooper.get(this).processAllMessages();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDetach() {
|
public void testDetach() {
|
||||||
View v = new View(mContext);
|
|
||||||
LifecycleEventObserver observer = mock(LifecycleEventObserver.class);
|
LifecycleEventObserver observer = mock(LifecycleEventObserver.class);
|
||||||
LifecycleOwner lifecycle = viewAttachLifecycle(v);
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
lifecycle.getLifecycle().addObserver(observer);
|
lifecycle.getLifecycle().addObserver(observer);
|
||||||
|
|
||||||
ViewUtils.attachView(v);
|
ViewUtils.attachView(mView);
|
||||||
TestableLooper.get(this).processAllMessages();
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
|
||||||
ViewUtils.detachView(v);
|
ViewUtils.detachView(mView);
|
||||||
TestableLooper.get(this).processAllMessages();
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
|
||||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_PAUSE));
|
verify(observer).onStateChanged(eq(lifecycle), eq(ON_PAUSE));
|
||||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_STOP));
|
verify(observer).onStateChanged(eq(lifecycle), eq(ON_STOP));
|
||||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_DESTROY));
|
verify(observer).onStateChanged(eq(lifecycle), eq(ON_DESTROY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateBeforeAttach() {
|
||||||
|
// WHEN a lifecycle is obtained from a view
|
||||||
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
|
// THEN the lifecycle state should be INITIAZED
|
||||||
|
assertThat(lifecycle.getLifecycle().getCurrentState()).isEqualTo(
|
||||||
|
Lifecycle.State.INITIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateAfterAttach() {
|
||||||
|
// WHEN a lifecycle is obtained from a view
|
||||||
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
|
// AND the view is attached
|
||||||
|
ViewUtils.attachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
// THEN the lifecycle state should be RESUMED
|
||||||
|
assertThat(lifecycle.getLifecycle().getCurrentState()).isEqualTo(Lifecycle.State.RESUMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateAfterDetach() {
|
||||||
|
// WHEN a lifecycle is obtained from a view
|
||||||
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
|
// AND the view is detached
|
||||||
|
ViewUtils.attachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
ViewUtils.detachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
// THEN the lifecycle state should be DESTROYED
|
||||||
|
assertThat(lifecycle.getLifecycle().getCurrentState()).isEqualTo(Lifecycle.State.DESTROYED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateAfterReattach() {
|
||||||
|
// WHEN a lifecycle is obtained from a view
|
||||||
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
|
// AND the view is re-attached
|
||||||
|
ViewUtils.attachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
ViewUtils.detachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
ViewUtils.attachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
// THEN the lifecycle state should still be DESTROYED, err RESUMED?
|
||||||
|
assertThat(lifecycle.getLifecycle().getCurrentState()).isEqualTo(Lifecycle.State.RESUMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateWhenViewAlreadyAttached() {
|
||||||
|
// GIVEN that a view is already attached
|
||||||
|
ViewUtils.attachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
// WHEN a lifecycle is obtained from a view
|
||||||
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
|
// THEN the lifecycle state should be RESUMED
|
||||||
|
assertThat(lifecycle.getLifecycle().getCurrentState()).isEqualTo(Lifecycle.State.RESUMED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStateWhenViewAlreadyDetached() {
|
||||||
|
// GIVEN that a view is already detached
|
||||||
|
ViewUtils.attachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
ViewUtils.detachView(mView);
|
||||||
|
TestableLooper.get(this).processAllMessages();
|
||||||
|
// WHEN a lifecycle is obtained from a view
|
||||||
|
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||||
|
// THEN the lifecycle state should be INITIALIZED
|
||||||
|
assertThat(lifecycle.getLifecycle().getCurrentState()).isEqualTo(
|
||||||
|
Lifecycle.State.INITIALIZED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user