Merge "Add seek bar to QS Media Player" into rvc-dev am: b131e1b746
Change-Id: Ibdc8215a7b47927a9aae747bc5ee59e4c708e0bc
This commit is contained in:
@@ -136,6 +136,47 @@
|
||||
</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 -->
|
||||
<LinearLayout
|
||||
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;
|
||||
|
||||
import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaSession;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -28,12 +31,16 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.settingslib.media.MediaDevice;
|
||||
import com.android.systemui.R;
|
||||
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.util.concurrency.DelayableExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -54,6 +61,9 @@ public class QSMediaPlayer extends MediaControlPanel {
|
||||
};
|
||||
|
||||
private final QSPanel mParent;
|
||||
private final DelayableExecutor mBackgroundExecutor;
|
||||
private final SeekBarViewModel mSeekBarViewModel;
|
||||
private final SeekBarObserver mSeekBarObserver;
|
||||
|
||||
/**
|
||||
* Initialize quick shade version of player
|
||||
@@ -64,10 +74,20 @@ public class QSMediaPlayer extends MediaControlPanel {
|
||||
* @param backgroundExecutor
|
||||
*/
|
||||
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,
|
||||
backgroundExecutor);
|
||||
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);
|
||||
}
|
||||
|
||||
// Seek Bar
|
||||
final MediaController controller = new MediaController(getContext(), token);
|
||||
mBackgroundExecutor.execute(
|
||||
() -> mSeekBarViewModel.updateController(controller, iconColor));
|
||||
|
||||
// Set up long press menu
|
||||
View guts = mMediaNotifView.findViewById(R.id.media_guts);
|
||||
View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
|
||||
@@ -155,4 +180,16 @@ public class QSMediaPlayer extends MediaControlPanel {
|
||||
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.tuner.TunerService;
|
||||
import com.android.systemui.tuner.TunerService.Tunable;
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
@@ -103,7 +104,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
||||
private final NotificationMediaManager mNotificationMediaManager;
|
||||
private final LocalBluetoothManager mLocalBluetoothManager;
|
||||
private final Executor mForegroundExecutor;
|
||||
private final Executor mBackgroundExecutor;
|
||||
private final DelayableExecutor mBackgroundExecutor;
|
||||
private LocalMediaManager mLocalMediaManager;
|
||||
private MediaDevice mDevice;
|
||||
private boolean mUpdateCarousel = false;
|
||||
@@ -166,7 +167,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
||||
QSLogger qsLogger,
|
||||
NotificationMediaManager notificationMediaManager,
|
||||
@Main Executor foregroundExecutor,
|
||||
@Background Executor backgroundExecutor,
|
||||
@Background DelayableExecutor backgroundExecutor,
|
||||
@Nullable LocalBluetoothManager localBluetoothManager
|
||||
) {
|
||||
super(context, attrs);
|
||||
@@ -278,7 +279,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
||||
Log.d(TAG, "creating new player");
|
||||
player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
|
||||
mForegroundExecutor, mBackgroundExecutor);
|
||||
|
||||
player.setListening(mListening);
|
||||
if (player.isPlaying()) {
|
||||
mMediaCarousel.addView(player.getView(), 0, lp); // add in front
|
||||
} else {
|
||||
@@ -584,6 +585,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
||||
if (mListening) {
|
||||
refreshAllTiles();
|
||||
}
|
||||
for (QSMediaPlayer player : mMediaPlayers) {
|
||||
player.setListening(mListening);
|
||||
}
|
||||
}
|
||||
|
||||
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.Tunable;
|
||||
import com.android.systemui.util.Utils;
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -81,7 +82,7 @@ public class QuickQSPanel extends QSPanel {
|
||||
QSLogger qsLogger,
|
||||
NotificationMediaManager notificationMediaManager,
|
||||
@Main Executor foregroundExecutor,
|
||||
@Background Executor backgroundExecutor,
|
||||
@Background DelayableExecutor backgroundExecutor,
|
||||
@Nullable LocalBluetoothManager localBluetoothManager
|
||||
) {
|
||||
super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager,
|
||||
|
||||
@@ -48,6 +48,9 @@ public class SysuiLifecycle {
|
||||
|
||||
ViewLifecycle(View v) {
|
||||
v.addOnAttachStateChangeListener(this);
|
||||
if (v.isAttachedToWindow()) {
|
||||
mLifecycle.markState(RESUMED);
|
||||
}
|
||||
}
|
||||
|
||||
@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.tileimpl.QSTileImpl;
|
||||
import com.android.systemui.statusbar.NotificationMediaManager;
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -87,7 +88,7 @@ public class QSPanelTest extends SysuiTestCase {
|
||||
@Mock
|
||||
private Executor mForegroundExecutor;
|
||||
@Mock
|
||||
private Executor mBackgroundExecutor;
|
||||
private DelayableExecutor mBackgroundExecutor;
|
||||
@Mock
|
||||
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.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -35,12 +37,15 @@ import android.testing.TestableLooper.RunWithLooper;
|
||||
import android.testing.ViewUtils;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@@ -49,39 +54,122 @@ import org.junit.runner.RunWith;
|
||||
@SmallTest
|
||||
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
|
||||
public void testAttach() {
|
||||
View v = new View(mContext);
|
||||
LifecycleEventObserver observer = mock(LifecycleEventObserver.class);
|
||||
LifecycleOwner lifecycle = viewAttachLifecycle(v);
|
||||
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||
lifecycle.getLifecycle().addObserver(observer);
|
||||
|
||||
ViewUtils.attachView(v);
|
||||
ViewUtils.attachView(mView);
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
|
||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_CREATE));
|
||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_START));
|
||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_RESUME));
|
||||
|
||||
ViewUtils.detachView(v);
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetach() {
|
||||
View v = new View(mContext);
|
||||
LifecycleEventObserver observer = mock(LifecycleEventObserver.class);
|
||||
LifecycleOwner lifecycle = viewAttachLifecycle(v);
|
||||
LifecycleOwner lifecycle = viewAttachLifecycle(mView);
|
||||
lifecycle.getLifecycle().addObserver(observer);
|
||||
|
||||
ViewUtils.attachView(v);
|
||||
ViewUtils.attachView(mView);
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
|
||||
ViewUtils.detachView(v);
|
||||
ViewUtils.detachView(mView);
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
|
||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_PAUSE));
|
||||
verify(observer).onStateChanged(eq(lifecycle), eq(ON_STOP));
|
||||
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