Merge "Use RepeatableExecutor to poll playback position" into rvc-dev am: ed33661150 am: b9dd87981c

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

Change-Id: Idb915e8114fb158c853c9b390430316426e316b3
This commit is contained in:
TreeHugger Robot
2020-06-05 03:17:03 +00:00
committed by Automerger Merge Worker
6 changed files with 92 additions and 41 deletions

View File

@@ -49,15 +49,17 @@ import com.android.settingslib.Utils;
import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.media.MediaOutputSliceConstants;
import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.concurrency.DelayableExecutor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.inject.Inject;
/** /**
* A view controller used for Media Playback. * A view controller used for Media Playback.
*/ */
@@ -93,12 +95,14 @@ public class MediaControlPanel {
* @param backgroundExecutor background executor, used for processing artwork * @param backgroundExecutor background executor, used for processing artwork
* @param activityStarter activity starter * @param activityStarter activity starter
*/ */
public MediaControlPanel(Context context, DelayableExecutor backgroundExecutor, @Inject
ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager) { public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager,
SeekBarViewModel seekBarViewModel) {
mContext = context; mContext = context;
mBackgroundExecutor = backgroundExecutor; mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarViewModel = seekBarViewModel;
mMediaViewController = new MediaViewController(context, mediaHostStatesManager); mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
loadDimens(); loadDimens();
} }

View File

@@ -11,14 +11,12 @@ import android.widget.HorizontalScrollView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.PageIndicator import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.statusbar.notification.VisualStabilityManager
import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.animation.requiresRemeasuring
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
private const val FLING_SLOP = 1000000 private const val FLING_SLOP = 1000000
@@ -30,9 +28,8 @@ private const val FLING_SLOP = 1000000
@Singleton @Singleton
class MediaViewManager @Inject constructor( class MediaViewManager @Inject constructor(
private val context: Context, private val context: Context,
@Background private val backgroundExecutor: DelayableExecutor, private val mediaControlPanelFactory: Provider<MediaControlPanel>,
private val visualStabilityManager: VisualStabilityManager, private val visualStabilityManager: VisualStabilityManager,
private val activityStarter: ActivityStarter,
private val mediaHostStatesManager: MediaHostStatesManager, private val mediaHostStatesManager: MediaHostStatesManager,
mediaManager: MediaDataCombineLatest mediaManager: MediaDataCombineLatest
) { ) {
@@ -265,8 +262,7 @@ class MediaViewManager @Inject constructor(
} }
var existingPlayer = mediaPlayers[key] var existingPlayer = mediaPlayers[key]
if (existingPlayer == null) { if (existingPlayer == null) {
existingPlayer = MediaControlPanel(context, backgroundExecutor, activityStarter, existingPlayer = mediaControlPanelFactory.get()
mediaHostStatesManager)
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
mediaContent)) mediaContent))
mediaPlayers[key] = existingPlayer mediaPlayers[key] = existingPlayer
@@ -289,7 +285,7 @@ class MediaViewManager @Inject constructor(
needsReordering = true needsReordering = true
} }
} }
existingPlayer.bind(data) existingPlayer?.bind(data)
updateMediaPaddings() updateMediaPaddings()
updatePageIndicator() updatePageIndicator()
} }

View File

@@ -27,8 +27,10 @@ import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.RepeatableExecutor
import java.util.concurrent.Executor
import javax.inject.Inject
private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
@@ -65,7 +67,7 @@ private fun PlaybackState.computePosition(duration: Long): Long {
} }
/** ViewModel for seek bar in QS media player. */ /** ViewModel for seek bar in QS media player. */
class SeekBarViewModel(val bgExecutor: DelayableExecutor) { class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: RepeatableExecutor) {
private var _data = Progress(false, false, null, null) private var _data = Progress(false, false, null, null)
set(value) { set(value) {
@@ -89,10 +91,10 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
private var callback = object : MediaController.Callback() { private var callback = object : MediaController.Callback() {
override fun onPlaybackStateChanged(state: PlaybackState) { override fun onPlaybackStateChanged(state: PlaybackState) {
playbackState = state playbackState = state
if (shouldPollPlaybackPosition()) { if (PlaybackState.STATE_NONE.equals(playbackState)) {
checkPlaybackPosition()
} else if (PlaybackState.STATE_NONE.equals(playbackState)) {
clearController() clearController()
} else {
checkIfPollingNeeded()
} }
} }
@@ -100,12 +102,14 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
clearController() clearController()
} }
} }
private var cancel: Runnable? = null
/** Listening state (QS open or closed) is used to control polling of progress. */ /** Listening state (QS open or closed) is used to control polling of progress. */
var listening = true var listening = true
set(value) { set(value) = bgExecutor.execute {
if (value) { if (field != value) {
checkPlaybackPosition() field = value
checkIfPollingNeeded()
} }
} }
@@ -137,9 +141,7 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
playbackState?.getState() == PlaybackState.STATE_NONE || playbackState?.getState() == PlaybackState.STATE_NONE ||
(duration != null && duration <= 0)) false else true (duration != null && duration <= 0)) false else true
_data = Progress(enabled, seekAvailable, position, duration) _data = Progress(enabled, seekAvailable, position, duration)
if (shouldPollPlaybackPosition()) { checkIfPollingNeeded()
checkPlaybackPosition()
}
} }
/** /**
@@ -151,6 +153,8 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
fun clearController() = bgExecutor.execute { fun clearController() = bgExecutor.execute {
controller = null controller = null
playbackState = null playbackState = null
cancel?.run()
cancel = null
_data = _data.copy(enabled = false) _data = _data.copy(enabled = false)
} }
@@ -158,26 +162,34 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
* Call to clean up any resources. * Call to clean up any resources.
*/ */
@AnyThread @AnyThread
fun onDestroy() { fun onDestroy() = bgExecutor.execute {
controller = null controller = null
playbackState = null playbackState = null
cancel?.run()
cancel = null
} }
@AnyThread @WorkerThread
private fun checkPlaybackPosition(): Runnable = bgExecutor.executeDelayed({ private fun checkPlaybackPosition() {
val duration = _data.duration ?: -1 val duration = _data.duration ?: -1
val currentPosition = playbackState?.computePosition(duration.toLong())?.toInt() val currentPosition = playbackState?.computePosition(duration.toLong())?.toInt()
if (currentPosition != null && _data.elapsedTime != currentPosition) { if (currentPosition != null && _data.elapsedTime != currentPosition) {
_data = _data.copy(elapsedTime = currentPosition) _data = _data.copy(elapsedTime = currentPosition)
} }
if (shouldPollPlaybackPosition()) {
checkPlaybackPosition()
} }
}, POSITION_UPDATE_INTERVAL_MILLIS)
@WorkerThread @WorkerThread
private fun shouldPollPlaybackPosition(): Boolean { private fun checkIfPollingNeeded() {
return listening && playbackState?.isInMotion() ?: false val needed = listening && playbackState?.isInMotion() ?: false
if (needed) {
if (cancel == null) {
cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L,
POSITION_UPDATE_INTERVAL_MILLIS)
}
} else {
cancel?.run()
cancel = null
}
} }
/** Gets a listener to attach to the seek bar to handle seeking. */ /** Gets a listener to attach to the seek bar to handle seeking. */
@@ -194,7 +206,7 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
private class SeekBarChangeListener( private class SeekBarChangeListener(
val viewModel: SeekBarViewModel, val viewModel: SeekBarViewModel,
val bgExecutor: DelayableExecutor val bgExecutor: Executor
) : SeekBar.OnSeekBarChangeListener { ) : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) { if (fromUser) {

View File

@@ -31,6 +31,7 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
@@ -41,6 +42,7 @@ import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
@@ -48,6 +50,7 @@ import org.mockito.Mock
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
private const val KEY = "TEST_KEY" private const val KEY = "TEST_KEY"
private const val APP = "APP" private const val APP = "APP"
@@ -73,6 +76,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var holder: PlayerViewHolder @Mock private lateinit var holder: PlayerViewHolder
@Mock private lateinit var view: TransitionLayout @Mock private lateinit var view: TransitionLayout
@Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager @Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
@Mock private lateinit var seekBarViewModel: SeekBarViewModel
@Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
private lateinit var appIcon: ImageView private lateinit var appIcon: ImageView
private lateinit var appName: TextView private lateinit var appName: TextView
private lateinit var albumView: ImageView private lateinit var albumView: ImageView
@@ -94,18 +99,17 @@ public class MediaControlPanelTest : SysuiTestCase() {
private val device = MediaDeviceData(true, null, DEVICE_NAME) private val device = MediaDeviceData(true, null, DEVICE_NAME)
private val disabledDevice = MediaDeviceData(false, null, null) private val disabledDevice = MediaDeviceData(false, null, null)
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Before @Before
fun setUp() { fun setUp() {
bgExecutor = FakeExecutor(FakeSystemClock()) bgExecutor = FakeExecutor(FakeSystemClock())
activityStarter = mock(ActivityStarter::class.java) player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager,
mediaHostStatesManager = mock(MediaHostStatesManager::class.java) seekBarViewModel)
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager)
// Mock out a view holder for the player to attach to. // Mock out a view holder for the player to attach to.
holder = mock(PlayerViewHolder::class.java)
view = mock(TransitionLayout::class.java)
whenever(holder.player).thenReturn(view) whenever(holder.player).thenReturn(view)
appIcon = ImageView(context) appIcon = ImageView(context)
whenever(holder.appIcon).thenReturn(appIcon) whenever(holder.appIcon).thenReturn(appIcon)

View File

@@ -29,6 +29,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.concurrency.FakeRepeatableExecutor
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -71,7 +72,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
fakeExecutor = FakeExecutor(FakeSystemClock()) fakeExecutor = FakeExecutor(FakeSystemClock())
viewModel = SeekBarViewModel(fakeExecutor) viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor))
mockController = mock(MediaController::class.java) mockController = mock(MediaController::class.java)
whenever(mockController.sessionToken).thenReturn(token1) whenever(mockController.sessionToken).thenReturn(token1)
mockTransport = mock(MediaController.TransportControls::class.java) mockTransport = mock(MediaController.TransportControls::class.java)

View File

@@ -0,0 +1,34 @@
/*
* 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.util.concurrency;
/**
* A fake to use in tests.
*/
public class FakeRepeatableExecutor extends RepeatableExecutorImpl {
/**
* Initializes a fake RepeatableExecutor from a fake executor.
*
* Use the fake executor to actually process tasks.
*
* @param executor fake executor.
*/
public FakeRepeatableExecutor(FakeExecutor executor) {
super(executor);
}
}