Merge "towards MR2: update to new backend" into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
19f6945f56
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.Context
|
||||||
|
|
||||||
|
import com.android.settingslib.bluetooth.LocalBluetoothManager
|
||||||
|
import com.android.settingslib.media.InfoMediaManager
|
||||||
|
import com.android.settingslib.media.LocalMediaManager
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to create [LocalMediaManager] objects.
|
||||||
|
*/
|
||||||
|
class LocalMediaManagerFactory @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val localBluetoothManager: LocalBluetoothManager?
|
||||||
|
) {
|
||||||
|
/** Creates a [LocalMediaManager] for the given package. */
|
||||||
|
fun create(packageName: String): LocalMediaManager {
|
||||||
|
return InfoMediaManager(context, packageName, null, localBluetoothManager).run {
|
||||||
|
LocalMediaManager(context, localBluetoothManager, this, packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,6 @@ import android.util.Log;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@@ -56,14 +55,11 @@ import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
|||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||||
|
|
||||||
import com.android.settingslib.Utils;
|
import com.android.settingslib.Utils;
|
||||||
import com.android.settingslib.media.LocalMediaManager;
|
|
||||||
import com.android.settingslib.media.MediaDevice;
|
|
||||||
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.plugins.ActivityStarter;
|
import com.android.systemui.plugins.ActivityStarter;
|
||||||
import com.android.systemui.qs.QSMediaBrowser;
|
import com.android.systemui.qs.QSMediaBrowser;
|
||||||
import com.android.systemui.util.Assert;
|
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -77,7 +73,6 @@ import java.util.concurrent.Executor;
|
|||||||
*/
|
*/
|
||||||
public class MediaControlPanel {
|
public class MediaControlPanel {
|
||||||
private static final String TAG = "MediaControlPanel";
|
private static final String TAG = "MediaControlPanel";
|
||||||
@Nullable private final LocalMediaManager mLocalMediaManager;
|
|
||||||
|
|
||||||
// Button IDs for QS controls
|
// Button IDs for QS controls
|
||||||
static final int[] ACTION_IDS = {
|
static final int[] ACTION_IDS = {
|
||||||
@@ -100,7 +95,6 @@ public class MediaControlPanel {
|
|||||||
private MediaSession.Token mToken;
|
private MediaSession.Token mToken;
|
||||||
private MediaController mController;
|
private MediaController mController;
|
||||||
private int mBackgroundColor;
|
private int mBackgroundColor;
|
||||||
private MediaDevice mDevice;
|
|
||||||
protected ComponentName mServiceComponent;
|
protected ComponentName mServiceComponent;
|
||||||
private boolean mIsRegistered = false;
|
private boolean mIsRegistered = false;
|
||||||
private List<KeyFrames> mKeyFrames;
|
private List<KeyFrames> mKeyFrames;
|
||||||
@@ -113,7 +107,6 @@ public class MediaControlPanel {
|
|||||||
public static final String MEDIA_PREFERENCE_KEY = "browser_components";
|
public static final String MEDIA_PREFERENCE_KEY = "browser_components";
|
||||||
private SharedPreferences mSharedPrefs;
|
private SharedPreferences mSharedPrefs;
|
||||||
private boolean mCheckedForResumption = false;
|
private boolean mCheckedForResumption = false;
|
||||||
private boolean mIsRemotePlayback;
|
|
||||||
private QSMediaBrowser mQSMediaBrowser;
|
private QSMediaBrowser mQSMediaBrowser;
|
||||||
|
|
||||||
private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
|
private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
|
||||||
@@ -122,7 +115,6 @@ public class MediaControlPanel {
|
|||||||
Log.d(TAG, "session destroyed");
|
Log.d(TAG, "session destroyed");
|
||||||
mController.unregisterCallback(mSessionCallback);
|
mController.unregisterCallback(mSessionCallback);
|
||||||
clearControls();
|
clearControls();
|
||||||
makeInactive();
|
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackStateChanged(PlaybackState state) {
|
public void onPlaybackStateChanged(PlaybackState state) {
|
||||||
@@ -130,31 +122,6 @@ public class MediaControlPanel {
|
|||||||
if (s == PlaybackState.STATE_NONE) {
|
if (s == PlaybackState.STATE_NONE) {
|
||||||
Log.d(TAG, "playback state change will trigger resumption, state=" + state);
|
Log.d(TAG, "playback state change will trigger resumption, state=" + state);
|
||||||
clearControls();
|
clearControls();
|
||||||
makeInactive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final LocalMediaManager.DeviceCallback mDeviceCallback =
|
|
||||||
new LocalMediaManager.DeviceCallback() {
|
|
||||||
@Override
|
|
||||||
public void onDeviceListUpdate(List<MediaDevice> devices) {
|
|
||||||
if (mLocalMediaManager == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MediaDevice currentDevice = mLocalMediaManager.getCurrentConnectedDevice();
|
|
||||||
// Check because this can be called several times while changing devices
|
|
||||||
if (mDevice == null || !mDevice.equals(currentDevice)) {
|
|
||||||
mDevice = currentDevice;
|
|
||||||
updateDevice(mDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
|
|
||||||
if (mDevice == null || !mDevice.equals(device)) {
|
|
||||||
mDevice = device;
|
|
||||||
updateDevice(mDevice);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -162,16 +129,13 @@ public class MediaControlPanel {
|
|||||||
/**
|
/**
|
||||||
* Initialize a new control panel
|
* Initialize a new control panel
|
||||||
* @param context
|
* @param context
|
||||||
* @param routeManager Manager used to listen for device change events.
|
|
||||||
* @param foregroundExecutor foreground executor
|
* @param foregroundExecutor foreground executor
|
||||||
* @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, @Nullable LocalMediaManager routeManager,
|
public MediaControlPanel(Context context, Executor foregroundExecutor,
|
||||||
Executor foregroundExecutor, DelayableExecutor backgroundExecutor,
|
DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
|
||||||
ActivityStarter activityStarter) {
|
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mLocalMediaManager = routeManager;
|
|
||||||
mForegroundExecutor = foregroundExecutor;
|
mForegroundExecutor = foregroundExecutor;
|
||||||
mBackgroundExecutor = backgroundExecutor;
|
mBackgroundExecutor = backgroundExecutor;
|
||||||
mActivityStarter = activityStarter;
|
mActivityStarter = activityStarter;
|
||||||
@@ -183,7 +147,6 @@ public class MediaControlPanel {
|
|||||||
if (mSeekBarObserver != null) {
|
if (mSeekBarObserver != null) {
|
||||||
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
|
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
|
||||||
}
|
}
|
||||||
makeInactive();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadDimens() {
|
private void loadDimens() {
|
||||||
@@ -318,11 +281,9 @@ public class MediaControlPanel {
|
|||||||
artistText.setText(data.getArtist());
|
artistText.setText(data.getArtist());
|
||||||
|
|
||||||
// Transfer chip
|
// Transfer chip
|
||||||
if (mLocalMediaManager != null) {
|
|
||||||
mViewHolder.getSeamless().setVisibility(View.VISIBLE);
|
mViewHolder.getSeamless().setVisibility(View.VISIBLE);
|
||||||
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
|
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
|
||||||
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
|
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
|
||||||
updateDevice(mLocalMediaManager.getCurrentConnectedDevice());
|
|
||||||
mViewHolder.getSeamless().setOnClickListener(v -> {
|
mViewHolder.getSeamless().setOnClickListener(v -> {
|
||||||
final Intent intent = new Intent()
|
final Intent intent = new Intent()
|
||||||
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
|
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
|
||||||
@@ -332,16 +293,55 @@ public class MediaControlPanel {
|
|||||||
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
|
mActivityStarter.startActivity(intent, false, true /* dismissShade */,
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
});
|
});
|
||||||
} else {
|
final boolean isRemotePlayback;
|
||||||
Log.d(TAG, "LocalMediaManager is null. Not binding output chip for pkg=" + pkgName);
|
|
||||||
}
|
|
||||||
PlaybackInfo playbackInfo = mController.getPlaybackInfo();
|
PlaybackInfo playbackInfo = mController.getPlaybackInfo();
|
||||||
if (playbackInfo != null) {
|
if (playbackInfo != null) {
|
||||||
mIsRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
|
isRemotePlayback = playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
|
Log.d(TAG, "PlaybackInfo was null. Defaulting to local playback.");
|
||||||
mIsRemotePlayback = false;
|
isRemotePlayback = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageView iconView = mViewHolder.getSeamlessIcon();
|
||||||
|
TextView deviceName = mViewHolder.getSeamlessText();
|
||||||
|
|
||||||
|
// Update the outline color
|
||||||
|
RippleDrawable bkgDrawable = (RippleDrawable) mViewHolder.getSeamless().getBackground();
|
||||||
|
GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
|
||||||
|
rect.setStroke(2, deviceName.getCurrentTextColor());
|
||||||
|
rect.setColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
if (isRemotePlayback) {
|
||||||
|
mViewHolder.getSeamless().setEnabled(false);
|
||||||
|
// TODO(b/156875717): setEnabled should cause the alpha to change.
|
||||||
|
mViewHolder.getSeamless().setAlpha(0.38f);
|
||||||
|
iconView.setImageResource(R.drawable.ic_hardware_speaker);
|
||||||
|
iconView.setVisibility(View.VISIBLE);
|
||||||
|
deviceName.setText(R.string.media_seamless_remote_device);
|
||||||
|
} else if (data.getDevice() != null && data.getDevice().getIcon() != null
|
||||||
|
&& data.getDevice().getName() != null) {
|
||||||
|
mViewHolder.getSeamless().setEnabled(true);
|
||||||
|
mViewHolder.getSeamless().setAlpha(1f);
|
||||||
|
Drawable icon = data.getDevice().getIcon();
|
||||||
|
iconView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
if (icon instanceof AdaptiveIcon) {
|
||||||
|
AdaptiveIcon aIcon = (AdaptiveIcon) icon;
|
||||||
|
aIcon.setBackgroundColor(mBackgroundColor);
|
||||||
|
iconView.setImageDrawable(aIcon);
|
||||||
|
} else {
|
||||||
|
iconView.setImageDrawable(icon);
|
||||||
|
}
|
||||||
|
deviceName.setText(data.getDevice().getName());
|
||||||
|
} else {
|
||||||
|
// Reset to default
|
||||||
|
Log.w(TAG, "device is null. Not binding output chip.");
|
||||||
|
mViewHolder.getSeamless().setEnabled(true);
|
||||||
|
mViewHolder.getSeamless().setAlpha(1f);
|
||||||
|
iconView.setVisibility(View.GONE);
|
||||||
|
deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
|
||||||
|
}
|
||||||
|
|
||||||
List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
|
List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
|
||||||
// Media controls
|
// Media controls
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -382,8 +382,6 @@ public class MediaControlPanel {
|
|||||||
// Set up long press menu
|
// Set up long press menu
|
||||||
// TODO: b/156036025 bring back media guts
|
// TODO: b/156036025 bring back media guts
|
||||||
|
|
||||||
makeActive();
|
|
||||||
|
|
||||||
// Update both constraint sets to regenerate the animation.
|
// Update both constraint sets to regenerate the animation.
|
||||||
mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
|
mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
|
||||||
mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
|
mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
|
||||||
@@ -514,60 +512,6 @@ public class MediaControlPanel {
|
|||||||
return (state.getState() == PlaybackState.STATE_PLAYING);
|
return (state.getState() == PlaybackState.STATE_PLAYING);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the current device information
|
|
||||||
* @param device device information to display
|
|
||||||
*/
|
|
||||||
private void updateDevice(MediaDevice device) {
|
|
||||||
mForegroundExecutor.execute(() -> {
|
|
||||||
updateChipInternal(device);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateChipInternal(MediaDevice device) {
|
|
||||||
if (mViewHolder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ImageView iconView = mViewHolder.getSeamlessIcon();
|
|
||||||
TextView deviceName = mViewHolder.getSeamlessText();
|
|
||||||
|
|
||||||
// Update the outline color
|
|
||||||
LinearLayout viewLayout = (LinearLayout) mViewHolder.getSeamless();
|
|
||||||
RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
|
|
||||||
GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
|
|
||||||
rect.setStroke(2, deviceName.getCurrentTextColor());
|
|
||||||
rect.setColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
if (mIsRemotePlayback) {
|
|
||||||
mViewHolder.getSeamless().setEnabled(false);
|
|
||||||
mViewHolder.getSeamless().setAlpha(0.38f);
|
|
||||||
iconView.setImageResource(R.drawable.ic_hardware_speaker);
|
|
||||||
iconView.setVisibility(View.VISIBLE);
|
|
||||||
deviceName.setText(R.string.media_seamless_remote_device);
|
|
||||||
} else if (device != null) {
|
|
||||||
mViewHolder.getSeamless().setEnabled(true);
|
|
||||||
mViewHolder.getSeamless().setAlpha(1f);
|
|
||||||
Drawable icon = device.getIcon();
|
|
||||||
iconView.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
if (icon instanceof AdaptiveIcon) {
|
|
||||||
AdaptiveIcon aIcon = (AdaptiveIcon) icon;
|
|
||||||
aIcon.setBackgroundColor(mBackgroundColor);
|
|
||||||
iconView.setImageDrawable(aIcon);
|
|
||||||
} else {
|
|
||||||
iconView.setImageDrawable(icon);
|
|
||||||
}
|
|
||||||
deviceName.setText(device.getName());
|
|
||||||
} else {
|
|
||||||
// Reset to default
|
|
||||||
Log.d(TAG, "device is null. Not binding output chip.");
|
|
||||||
mViewHolder.getSeamless().setEnabled(true);
|
|
||||||
mViewHolder.getSeamless().setAlpha(1f);
|
|
||||||
iconView.setVisibility(View.GONE);
|
|
||||||
deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts controls into a resumption state if possible, or calls removePlayer if no component was
|
* Puts controls into a resumption state if possible, or calls removePlayer if no component was
|
||||||
* found that could resume playback
|
* found that could resume playback
|
||||||
@@ -642,27 +586,6 @@ public class MediaControlPanel {
|
|||||||
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
|
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void makeActive() {
|
|
||||||
Assert.isMainThread();
|
|
||||||
if (!mIsRegistered) {
|
|
||||||
if (mLocalMediaManager != null) {
|
|
||||||
mLocalMediaManager.registerCallback(mDeviceCallback);
|
|
||||||
mLocalMediaManager.startScan();
|
|
||||||
}
|
|
||||||
mIsRegistered = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void makeInactive() {
|
|
||||||
Assert.isMainThread();
|
|
||||||
if (mIsRegistered) {
|
|
||||||
if (mLocalMediaManager != null) {
|
|
||||||
mLocalMediaManager.stopScan();
|
|
||||||
mLocalMediaManager.unregisterCallback(mDeviceCallback);
|
|
||||||
}
|
|
||||||
mIsRegistered = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Verify that we can connect to the given component with a MediaBrowser, and if so, add that
|
* Verify that we can connect to the given component with a MediaBrowser, and if so, add that
|
||||||
* component to the list of resumption components
|
* component to the list of resumption components
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ data class MediaData(
|
|||||||
val actionsToShowInCompact: List<Int>,
|
val actionsToShowInCompact: List<Int>,
|
||||||
val packageName: String?,
|
val packageName: String?,
|
||||||
val token: MediaSession.Token?,
|
val token: MediaSession.Token?,
|
||||||
val clickIntent: PendingIntent?
|
val clickIntent: PendingIntent?,
|
||||||
|
val device: MediaDeviceData?
|
||||||
)
|
)
|
||||||
|
|
||||||
/** State of a media action. */
|
/** State of a media action. */
|
||||||
@@ -43,3 +44,9 @@ data class MediaAction(
|
|||||||
val intent: PendingIntent?,
|
val intent: PendingIntent?,
|
||||||
val contentDescription: CharSequence?
|
val contentDescription: CharSequence?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** State of the media device. */
|
||||||
|
data class MediaDeviceData(
|
||||||
|
val icon: Drawable?,
|
||||||
|
val name: String?
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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 javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines updates from [MediaDataManager] with [MediaDeviceManager].
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class MediaDataCombineLatest @Inject constructor(
|
||||||
|
private val dataSource: MediaDataManager,
|
||||||
|
private val deviceSource: MediaDeviceManager
|
||||||
|
) {
|
||||||
|
private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
|
||||||
|
private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataSource.addListener(object : MediaDataManager.Listener {
|
||||||
|
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
||||||
|
entries[key] = data to entries[key]?.second
|
||||||
|
update(key)
|
||||||
|
}
|
||||||
|
override fun onMediaDataRemoved(key: String) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
deviceSource.addListener(object : MediaDeviceManager.Listener {
|
||||||
|
override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) {
|
||||||
|
entries[key] = entries[key]?.first to data
|
||||||
|
update(key)
|
||||||
|
}
|
||||||
|
override fun onKeyRemoved(key: String) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData].
|
||||||
|
*/
|
||||||
|
fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a listener registered with addListener.
|
||||||
|
*/
|
||||||
|
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
|
||||||
|
|
||||||
|
private fun update(key: String) {
|
||||||
|
val (entry, device) = entries[key] ?: null to null
|
||||||
|
if (entry != null && device != null) {
|
||||||
|
val data = entry.copy(device = device)
|
||||||
|
listeners.forEach {
|
||||||
|
it.onMediaDataLoaded(key, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun remove(key: String) {
|
||||||
|
entries.remove(key)?.let {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onMediaDataRemoved(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,19 @@ private const val LUMINOSITY_THRESHOLD = 0.05f
|
|||||||
private const val SATURATION_MULTIPLIER = 0.8f
|
private const val SATURATION_MULTIPLIER = 0.8f
|
||||||
|
|
||||||
private val LOADING = MediaData(false, 0, null, null, null, null, null,
|
private val LOADING = MediaData(false, 0, null, null, null, null, null,
|
||||||
emptyList(), emptyList(), null, null, null)
|
emptyList(), emptyList(), null, null, null, null)
|
||||||
|
|
||||||
|
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||||
|
if (!sbn.notification.hasMediaSession()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val notificationStyle = sbn.notification.notificationStyle
|
||||||
|
if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) ||
|
||||||
|
Notification.MediaStyle::class.java.equals(notificationStyle)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that facilitates management and loading of Media Data, ready for binding.
|
* A class that facilitates management and loading of Media Data, ready for binding.
|
||||||
@@ -72,7 +84,7 @@ class MediaDataManager @Inject constructor(
|
|||||||
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||||
|
|
||||||
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
||||||
if (isMediaNotification(sbn)) {
|
if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
|
||||||
if (!mediaEntries.containsKey(key)) {
|
if (!mediaEntries.containsKey(key)) {
|
||||||
mediaEntries.put(key, LOADING)
|
mediaEntries.put(key, LOADING)
|
||||||
}
|
}
|
||||||
@@ -204,7 +216,7 @@ class MediaDataManager @Inject constructor(
|
|||||||
foregroundExecutor.execute {
|
foregroundExecutor.execute {
|
||||||
onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
|
onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
|
||||||
artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
||||||
notif.contentIntent))
|
notif.contentIntent, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,21 +282,6 @@ class MediaDataManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
|
||||||
if (!Utils.useQsMediaPlayer(context)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!sbn.notification.hasMediaSession()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val notificationStyle = sbn.notification.notificationStyle
|
|
||||||
if (Notification.DecoratedMediaCustomViewStyle::class.java.equals(notificationStyle) ||
|
|
||||||
Notification.MediaStyle::class.java.equals(notificationStyle)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are there any media notifications active?
|
* Are there any media notifications active?
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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.Context
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import com.android.settingslib.media.LocalMediaManager
|
||||||
|
import com.android.settingslib.media.MediaDevice
|
||||||
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides information about the route (ie. device) where playback is occurring.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class MediaDeviceManager @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val localMediaManagerFactory: LocalMediaManagerFactory,
|
||||||
|
private val featureFlag: MediaFeatureFlag,
|
||||||
|
@Main private val fgExecutor: Executor
|
||||||
|
) {
|
||||||
|
private val listeners: MutableSet<Listener> = mutableSetOf()
|
||||||
|
private val entries: MutableMap<String, Token> = mutableMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a listener for changes to the media route (ie. device).
|
||||||
|
*/
|
||||||
|
fun addListener(listener: Listener) = listeners.add(listener)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a listener that has been registered with addListener.
|
||||||
|
*/
|
||||||
|
fun removeListener(listener: Listener) = listeners.remove(listener)
|
||||||
|
|
||||||
|
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
||||||
|
if (featureFlag.enabled && isMediaNotification(sbn)) {
|
||||||
|
var tok = entries[key]
|
||||||
|
if (tok == null) {
|
||||||
|
tok = Token(key, localMediaManagerFactory.create(sbn.packageName))
|
||||||
|
entries[key] = tok
|
||||||
|
tok.start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onNotificationRemoved(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNotificationRemoved(key: String) {
|
||||||
|
val token = entries.remove(key)
|
||||||
|
token?.stop()
|
||||||
|
token?.let {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onKeyRemoved(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processDevice(key: String, device: MediaDevice?) {
|
||||||
|
val data = MediaDeviceData(device?.icon, device?.name)
|
||||||
|
listeners.forEach {
|
||||||
|
it.onMediaDeviceChanged(key, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
/** Called when the route has changed for a given notification. */
|
||||||
|
fun onMediaDeviceChanged(key: String, data: MediaDeviceData?)
|
||||||
|
/** Called when the notification was removed. */
|
||||||
|
fun onKeyRemoved(key: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Token(
|
||||||
|
val key: String,
|
||||||
|
val localMediaManager: LocalMediaManager
|
||||||
|
) : LocalMediaManager.DeviceCallback {
|
||||||
|
private var current: MediaDevice? = null
|
||||||
|
set(value) {
|
||||||
|
if (value != field) {
|
||||||
|
field = value
|
||||||
|
processDevice(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun start() {
|
||||||
|
localMediaManager.registerCallback(this)
|
||||||
|
localMediaManager.startScan()
|
||||||
|
current = localMediaManager.getCurrentConnectedDevice()
|
||||||
|
}
|
||||||
|
fun stop() {
|
||||||
|
localMediaManager.stopScan()
|
||||||
|
localMediaManager.unregisterCallback(this)
|
||||||
|
}
|
||||||
|
override fun onDeviceListUpdate(devices: List<MediaDevice>?) = fgExecutor.execute {
|
||||||
|
current = localMediaManager.getCurrentConnectedDevice()
|
||||||
|
}
|
||||||
|
override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
|
||||||
|
fgExecutor.execute {
|
||||||
|
current = device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.Context
|
||||||
|
import com.android.systemui.util.Utils
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to the current value of the feature flag.
|
||||||
|
*/
|
||||||
|
class MediaFeatureFlag @Inject constructor(private val context: Context) {
|
||||||
|
val enabled
|
||||||
|
get() = Utils.useQsMediaPlayer(context)
|
||||||
|
}
|
||||||
@@ -6,9 +6,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.HorizontalScrollView
|
import android.widget.HorizontalScrollView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import com.android.settingslib.bluetooth.LocalBluetoothManager
|
|
||||||
import com.android.settingslib.media.InfoMediaManager
|
|
||||||
import com.android.settingslib.media.LocalMediaManager
|
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.dagger.qualifiers.Background
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
import com.android.systemui.dagger.qualifiers.Main
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
@@ -30,10 +27,9 @@ class MediaViewManager @Inject constructor(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
@Main private val foregroundExecutor: Executor,
|
@Main private val foregroundExecutor: Executor,
|
||||||
@Background private val backgroundExecutor: DelayableExecutor,
|
@Background private val backgroundExecutor: DelayableExecutor,
|
||||||
private val localBluetoothManager: LocalBluetoothManager?,
|
|
||||||
private val visualStabilityManager: VisualStabilityManager,
|
private val visualStabilityManager: VisualStabilityManager,
|
||||||
private val activityStarter: ActivityStarter,
|
private val activityStarter: ActivityStarter,
|
||||||
mediaManager: MediaDataManager
|
mediaManager: MediaDataCombineLatest
|
||||||
) {
|
) {
|
||||||
private var playerWidth: Int = 0
|
private var playerWidth: Int = 0
|
||||||
private var playerWidthPlusPadding: Int = 0
|
private var playerWidthPlusPadding: Int = 0
|
||||||
@@ -151,15 +147,8 @@ class MediaViewManager @Inject constructor(
|
|||||||
private fun updateView(key: String, data: MediaData) {
|
private fun updateView(key: String, data: MediaData) {
|
||||||
var existingPlayer = mediaPlayers[key]
|
var existingPlayer = mediaPlayers[key]
|
||||||
if (existingPlayer == null) {
|
if (existingPlayer == null) {
|
||||||
// Set up listener for device changes
|
existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
|
||||||
// TODO: integrate with MediaTransferManager?
|
activityStarter)
|
||||||
val imm = InfoMediaManager(context, data.packageName,
|
|
||||||
null /* notification */, localBluetoothManager)
|
|
||||||
val routeManager = LocalMediaManager(context, localBluetoothManager,
|
|
||||||
imm, data.packageName)
|
|
||||||
|
|
||||||
existingPlayer = MediaControlPanel(context, routeManager, foregroundExecutor,
|
|
||||||
backgroundExecutor, activityStarter)
|
|
||||||
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
||||||
mediaContent))
|
mediaContent))
|
||||||
mediaPlayers[key] = existingPlayer
|
mediaPlayers[key] = existingPlayer
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import com.android.systemui.Interpolators;
|
|||||||
import com.android.systemui.colorextraction.SysuiColorExtractor;
|
import com.android.systemui.colorextraction.SysuiColorExtractor;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
import com.android.systemui.media.MediaDataManager;
|
import com.android.systemui.media.MediaDataManager;
|
||||||
|
import com.android.systemui.media.MediaDeviceManager;
|
||||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.statusbar.dagger.StatusBarModule;
|
import com.android.systemui.statusbar.dagger.StatusBarModule;
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryListener;
|
import com.android.systemui.statusbar.notification.NotificationEntryListener;
|
||||||
@@ -218,7 +219,8 @@ public class NotificationMediaManager implements Dumpable {
|
|||||||
KeyguardBypassController keyguardBypassController,
|
KeyguardBypassController keyguardBypassController,
|
||||||
@Main DelayableExecutor mainExecutor,
|
@Main DelayableExecutor mainExecutor,
|
||||||
DeviceConfigProxy deviceConfig,
|
DeviceConfigProxy deviceConfig,
|
||||||
MediaDataManager mediaDataManager) {
|
MediaDataManager mediaDataManager,
|
||||||
|
MediaDeviceManager mediaDeviceManager) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mMediaArtworkProcessor = mediaArtworkProcessor;
|
mMediaArtworkProcessor = mediaArtworkProcessor;
|
||||||
mKeyguardBypassController = keyguardBypassController;
|
mKeyguardBypassController = keyguardBypassController;
|
||||||
@@ -239,11 +241,13 @@ public class NotificationMediaManager implements Dumpable {
|
|||||||
@Override
|
@Override
|
||||||
public void onPendingEntryAdded(NotificationEntry entry) {
|
public void onPendingEntryAdded(NotificationEntry entry) {
|
||||||
mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
|
mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
|
||||||
|
mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreEntryUpdated(NotificationEntry entry) {
|
public void onPreEntryUpdated(NotificationEntry entry) {
|
||||||
mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
|
mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
|
||||||
|
mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -264,6 +268,7 @@ public class NotificationMediaManager implements Dumpable {
|
|||||||
int reason) {
|
int reason) {
|
||||||
onNotificationRemoved(entry.getKey());
|
onNotificationRemoved(entry.getKey());
|
||||||
mediaDataManager.onNotificationRemoved(entry.getKey());
|
mediaDataManager.onNotificationRemoved(entry.getKey());
|
||||||
|
mediaDeviceManager.onNotificationRemoved(entry.getKey());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.android.internal.statusbar.IStatusBarService;
|
|||||||
import com.android.systemui.bubbles.BubbleController;
|
import com.android.systemui.bubbles.BubbleController;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
import com.android.systemui.media.MediaDataManager;
|
import com.android.systemui.media.MediaDataManager;
|
||||||
|
import com.android.systemui.media.MediaDeviceManager;
|
||||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.statusbar.ActionClickLogger;
|
import com.android.systemui.statusbar.ActionClickLogger;
|
||||||
import com.android.systemui.statusbar.CommandQueue;
|
import com.android.systemui.statusbar.CommandQueue;
|
||||||
@@ -101,7 +102,8 @@ public interface StatusBarDependenciesModule {
|
|||||||
KeyguardBypassController keyguardBypassController,
|
KeyguardBypassController keyguardBypassController,
|
||||||
@Main DelayableExecutor mainExecutor,
|
@Main DelayableExecutor mainExecutor,
|
||||||
DeviceConfigProxy deviceConfigProxy,
|
DeviceConfigProxy deviceConfigProxy,
|
||||||
MediaDataManager mediaDataManager) {
|
MediaDataManager mediaDataManager,
|
||||||
|
MediaDeviceManager mediaDeviceManager) {
|
||||||
return new NotificationMediaManager(
|
return new NotificationMediaManager(
|
||||||
context,
|
context,
|
||||||
statusBarLazy,
|
statusBarLazy,
|
||||||
@@ -111,7 +113,8 @@ public interface StatusBarDependenciesModule {
|
|||||||
keyguardBypassController,
|
keyguardBypassController,
|
||||||
mainExecutor,
|
mainExecutor,
|
||||||
deviceConfigProxy,
|
deviceConfigProxy,
|
||||||
mediaDataManager);
|
mediaDataManager,
|
||||||
|
mediaDeviceManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ package com.android.systemui.media
|
|||||||
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.graphics.drawable.RippleDrawable
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.media.session.MediaSession
|
import android.media.session.MediaSession
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
@@ -104,7 +106,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
activityStarter = mock(ActivityStarter::class.java)
|
activityStarter = mock(ActivityStarter::class.java)
|
||||||
|
|
||||||
player = MediaControlPanel(context, null, fgExecutor, bgExecutor, activityStarter)
|
player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter)
|
||||||
|
|
||||||
// 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)
|
holder = mock(PlayerViewHolder::class.java)
|
||||||
@@ -129,6 +131,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
artistText = TextView(context)
|
artistText = TextView(context)
|
||||||
whenever(holder.artistText).thenReturn(artistText)
|
whenever(holder.artistText).thenReturn(artistText)
|
||||||
seamless = FrameLayout(context)
|
seamless = FrameLayout(context)
|
||||||
|
val seamlessBackground = mock(RippleDrawable::class.java)
|
||||||
|
seamless.setBackground(seamlessBackground)
|
||||||
|
whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
|
||||||
whenever(holder.seamless).thenReturn(seamless)
|
whenever(holder.seamless).thenReturn(seamless)
|
||||||
seamlessIcon = ImageView(context)
|
seamlessIcon = ImageView(context)
|
||||||
whenever(holder.seamlessIcon).thenReturn(seamlessIcon)
|
whenever(holder.seamlessIcon).thenReturn(seamlessIcon)
|
||||||
@@ -176,7 +181,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun bindWhenUnattached() {
|
fun bindWhenUnattached() {
|
||||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||||
emptyList(), PACKAGE, null, null)
|
emptyList(), PACKAGE, null, null, MediaDeviceData(null, DEVICE_NAME))
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(player.isPlaying()).isFalse()
|
assertThat(player.isPlaying()).isFalse()
|
||||||
}
|
}
|
||||||
@@ -185,7 +190,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
fun bindText() {
|
fun bindText() {
|
||||||
player.attach(holder)
|
player.attach(holder)
|
||||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||||
emptyList(), PACKAGE, session.getSessionToken(), null)
|
emptyList(), PACKAGE, session.getSessionToken(), null,
|
||||||
|
MediaDeviceData(null, DEVICE_NAME))
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(appName.getText()).isEqualTo(APP)
|
assertThat(appName.getText()).isEqualTo(APP)
|
||||||
assertThat(titleText.getText()).isEqualTo(TITLE)
|
assertThat(titleText.getText()).isEqualTo(TITLE)
|
||||||
@@ -196,7 +202,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
fun bindBackgroundColor() {
|
fun bindBackgroundColor() {
|
||||||
player.attach(holder)
|
player.attach(holder)
|
||||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||||
emptyList(), PACKAGE, session.getSessionToken(), null)
|
emptyList(), PACKAGE, session.getSessionToken(), null,
|
||||||
|
MediaDeviceData(null, DEVICE_NAME))
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(background.getBackgroundTintList()).isEqualTo(ColorStateList.valueOf(BG_COLOR))
|
assertThat(background.getBackgroundTintList()).isEqualTo(ColorStateList.valueOf(BG_COLOR))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* 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 static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.any;
|
||||||
|
import static org.mockito.Mockito.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.testing.AndroidTestingRunner;
|
||||||
|
import android.testing.TestableLooper;
|
||||||
|
|
||||||
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
|
import com.android.systemui.SysuiTestCase;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner.class)
|
||||||
|
@TestableLooper.RunWithLooper
|
||||||
|
public class MediaDataCombineLatestTest extends SysuiTestCase {
|
||||||
|
|
||||||
|
private static final String KEY = "TEST_KEY";
|
||||||
|
private static final String APP = "APP";
|
||||||
|
private static final String PACKAGE = "PKG";
|
||||||
|
private static final int BG_COLOR = Color.RED;
|
||||||
|
private static final String ARTIST = "ARTIST";
|
||||||
|
private static final String TITLE = "TITLE";
|
||||||
|
private static final String DEVICE_NAME = "DEVICE_NAME";
|
||||||
|
|
||||||
|
private MediaDataCombineLatest mManager;
|
||||||
|
|
||||||
|
@Mock private MediaDataManager mDataSource;
|
||||||
|
@Mock private MediaDeviceManager mDeviceSource;
|
||||||
|
@Mock private MediaDataManager.Listener mListener;
|
||||||
|
|
||||||
|
private MediaDataManager.Listener mDataListener;
|
||||||
|
private MediaDeviceManager.Listener mDeviceListener;
|
||||||
|
|
||||||
|
private MediaData mMediaData;
|
||||||
|
private MediaDeviceData mDeviceData;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
mDataSource = mock(MediaDataManager.class);
|
||||||
|
mDeviceSource = mock(MediaDeviceManager.class);
|
||||||
|
mListener = mock(MediaDataManager.Listener.class);
|
||||||
|
|
||||||
|
mManager = new MediaDataCombineLatest(mDataSource, mDeviceSource);
|
||||||
|
|
||||||
|
mDataListener = captureDataListener();
|
||||||
|
mDeviceListener = captureDeviceListener();
|
||||||
|
|
||||||
|
mManager.addListener(mListener);
|
||||||
|
|
||||||
|
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||||
|
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null);
|
||||||
|
mDeviceData = new MediaDeviceData(null, DEVICE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void eventNotEmittedWithoutDevice() {
|
||||||
|
// WHEN data source emits an event without device data
|
||||||
|
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
||||||
|
// THEN an event isn't emitted
|
||||||
|
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void eventNotEmittedWithoutMedia() {
|
||||||
|
// WHEN device source emits an event without media data
|
||||||
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
|
// THEN an event isn't emitted
|
||||||
|
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emitEventAfterDeviceFirst() {
|
||||||
|
// GIVEN that a device event has already been received
|
||||||
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
|
// WHEN media event is received
|
||||||
|
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
||||||
|
// THEN the listener receives a combined event
|
||||||
|
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
||||||
|
verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
|
||||||
|
assertThat(captor.getValue().getDevice()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emitEventAfterMediaFirst() {
|
||||||
|
// GIVEN that media event has already been received
|
||||||
|
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
||||||
|
// WHEN device event is received
|
||||||
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
|
// THEN the listener receives a combined event
|
||||||
|
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
||||||
|
verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
|
||||||
|
assertThat(captor.getValue().getDevice()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mediaDataRemoved() {
|
||||||
|
// WHEN media data is removed without first receiving device or data
|
||||||
|
mDataListener.onMediaDataRemoved(KEY);
|
||||||
|
// THEN a removed event isn't emitted
|
||||||
|
verify(mListener, never()).onMediaDataRemoved(eq(KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mediaDataRemovedAfterMediaEvent() {
|
||||||
|
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
||||||
|
mDataListener.onMediaDataRemoved(KEY);
|
||||||
|
verify(mListener).onMediaDataRemoved(eq(KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mediaDataRemovedAfterDeviceEvent() {
|
||||||
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
|
mDataListener.onMediaDataRemoved(KEY);
|
||||||
|
verify(mListener).onMediaDataRemoved(eq(KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaDataManager.Listener captureDataListener() {
|
||||||
|
ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass(
|
||||||
|
MediaDataManager.Listener.class);
|
||||||
|
verify(mDataSource).addListener(captor.capture());
|
||||||
|
return captor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaDeviceManager.Listener captureDeviceListener() {
|
||||||
|
ArgumentCaptor<MediaDeviceManager.Listener> captor = ArgumentCaptor.forClass(
|
||||||
|
MediaDeviceManager.Listener.class);
|
||||||
|
verify(mDeviceSource).addListener(captor.capture());
|
||||||
|
return captor.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* 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.app.Notification
|
||||||
|
import android.media.MediaMetadata
|
||||||
|
import android.media.session.MediaSession
|
||||||
|
import android.media.session.PlaybackState
|
||||||
|
import android.os.Process
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.testing.TestableLooper
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
|
||||||
|
import com.android.settingslib.media.LocalMediaManager
|
||||||
|
import com.android.settingslib.media.MediaDevice
|
||||||
|
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.ArgumentCaptor
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.any
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when` as whenever
|
||||||
|
|
||||||
|
private const val KEY = "TEST_KEY"
|
||||||
|
private const val PACKAGE = "PKG"
|
||||||
|
private const val SESSION_KEY = "SESSION_KEY"
|
||||||
|
private const val SESSION_ARTIST = "SESSION_ARTIST"
|
||||||
|
private const val SESSION_TITLE = "SESSION_TITLE"
|
||||||
|
private const val DEVICE_NAME = "DEVICE_NAME"
|
||||||
|
|
||||||
|
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
@TestableLooper.RunWithLooper
|
||||||
|
public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
private lateinit var manager: MediaDeviceManager
|
||||||
|
|
||||||
|
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
|
||||||
|
@Mock private lateinit var lmm: LocalMediaManager
|
||||||
|
@Mock private lateinit var featureFlag: MediaFeatureFlag
|
||||||
|
private lateinit var fakeExecutor: FakeExecutor
|
||||||
|
|
||||||
|
@Mock private lateinit var device: MediaDevice
|
||||||
|
private lateinit var session: MediaSession
|
||||||
|
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||||
|
private lateinit var playbackBuilder: PlaybackState.Builder
|
||||||
|
private lateinit var notifBuilder: Notification.Builder
|
||||||
|
private lateinit var sbn: StatusBarNotification
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
lmmFactory = mock(LocalMediaManagerFactory::class.java)
|
||||||
|
lmm = mock(LocalMediaManager::class.java)
|
||||||
|
device = mock(MediaDevice::class.java)
|
||||||
|
whenever(device.name).thenReturn(DEVICE_NAME)
|
||||||
|
whenever(lmmFactory.create(PACKAGE)).thenReturn(lmm)
|
||||||
|
whenever(lmm.getCurrentConnectedDevice()).thenReturn(device)
|
||||||
|
featureFlag = mock(MediaFeatureFlag::class.java)
|
||||||
|
whenever(featureFlag.enabled).thenReturn(true)
|
||||||
|
|
||||||
|
fakeExecutor = FakeExecutor(FakeSystemClock())
|
||||||
|
|
||||||
|
manager = MediaDeviceManager(context, lmmFactory, featureFlag, fakeExecutor)
|
||||||
|
|
||||||
|
// Create a media sesssion and notification for testing.
|
||||||
|
metadataBuilder = MediaMetadata.Builder().apply {
|
||||||
|
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
|
||||||
|
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
|
||||||
|
}
|
||||||
|
playbackBuilder = PlaybackState.Builder().apply {
|
||||||
|
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
|
||||||
|
setActions(PlaybackState.ACTION_PLAY)
|
||||||
|
}
|
||||||
|
session = MediaSession(context, SESSION_KEY).apply {
|
||||||
|
setMetadata(metadataBuilder.build())
|
||||||
|
setPlaybackState(playbackBuilder.build())
|
||||||
|
}
|
||||||
|
session.setActive(true)
|
||||||
|
notifBuilder = Notification.Builder(context, "NONE").apply {
|
||||||
|
setContentTitle(SESSION_TITLE)
|
||||||
|
setContentText(SESSION_ARTIST)
|
||||||
|
setSmallIcon(android.R.drawable.ic_media_pause)
|
||||||
|
setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
|
||||||
|
}
|
||||||
|
sbn = StatusBarNotification(PACKAGE, PACKAGE, 0, "TAG", Process.myUid(), 0, 0,
|
||||||
|
notifBuilder.build(), Process.myUserHandle(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
session.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeUnknown() {
|
||||||
|
manager.onNotificationRemoved("unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addNotification() {
|
||||||
|
manager.onNotificationAdded(KEY, sbn)
|
||||||
|
verify(lmmFactory).create(PACKAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun featureDisabled() {
|
||||||
|
whenever(featureFlag.enabled).thenReturn(false)
|
||||||
|
manager.onNotificationAdded(KEY, sbn)
|
||||||
|
verify(lmmFactory, never()).create(PACKAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addAndRemoveNotification() {
|
||||||
|
manager.onNotificationAdded(KEY, sbn)
|
||||||
|
manager.onNotificationRemoved(KEY)
|
||||||
|
verify(lmm).unregisterCallback(any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deviceListUpdate() {
|
||||||
|
val listener = mock(MediaDeviceManager.Listener::class.java)
|
||||||
|
manager.addListener(listener)
|
||||||
|
manager.onNotificationAdded(KEY, sbn)
|
||||||
|
val deviceCallback = captureCallback()
|
||||||
|
// WHEN the device list changes
|
||||||
|
deviceCallback.onDeviceListUpdate(mutableListOf(device))
|
||||||
|
assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
|
||||||
|
// THEN the update is dispatched to the listener
|
||||||
|
val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
|
||||||
|
verify(listener).onMediaDeviceChanged(eq(KEY), captor.capture())
|
||||||
|
val data = captor.getValue()
|
||||||
|
assertThat(data.name).isEqualTo(DEVICE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun selectedDeviceStateChanged() {
|
||||||
|
val listener = mock(MediaDeviceManager.Listener::class.java)
|
||||||
|
manager.addListener(listener)
|
||||||
|
manager.onNotificationAdded(KEY, sbn)
|
||||||
|
val deviceCallback = captureCallback()
|
||||||
|
// WHEN the selected device changes state
|
||||||
|
deviceCallback.onSelectedDeviceStateChanged(device, 1)
|
||||||
|
assertThat(fakeExecutor.runAllReady()).isEqualTo(1)
|
||||||
|
// THEN the update is dispatched to the listener
|
||||||
|
val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
|
||||||
|
verify(listener).onMediaDeviceChanged(eq(KEY), captor.capture())
|
||||||
|
val data = captor.getValue()
|
||||||
|
assertThat(data.name).isEqualTo(DEVICE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun listenerReceivesKeyRemoved() {
|
||||||
|
manager.onNotificationAdded(KEY, sbn)
|
||||||
|
val listener = mock(MediaDeviceManager.Listener::class.java)
|
||||||
|
manager.addListener(listener)
|
||||||
|
// WHEN the notification is removed
|
||||||
|
manager.onNotificationRemoved(KEY)
|
||||||
|
// THEN the listener receives key removed event
|
||||||
|
verify(listener).onKeyRemoved(eq(KEY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun captureCallback(): LocalMediaManager.DeviceCallback {
|
||||||
|
val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
|
||||||
|
verify(lmm).registerCallback(captor.capture())
|
||||||
|
return captor.getValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user