Merge "Adding back media resumption" into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
ad80df940b
24
packages/SystemUI/res/drawable/ic_music_note.xml
Normal file
24
packages/SystemUI/res/drawable/ic_music_note.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
|
||||||
|
</vector>
|
||||||
@@ -2781,6 +2781,8 @@
|
|||||||
|
|
||||||
<!-- Close the controls associated with a specific media session [CHAR_LIMIT=NONE] -->
|
<!-- Close the controls associated with a specific media session [CHAR_LIMIT=NONE] -->
|
||||||
<string name="controls_media_close_session">Close this media session</string>
|
<string name="controls_media_close_session">Close this media session</string>
|
||||||
|
<!-- Label for button to resume media playback [CHAR_LIMIT=NONE] -->
|
||||||
|
<string name="controls_media_resume">Resume</string>
|
||||||
|
|
||||||
<!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
|
<!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
|
||||||
<string name="controls_error_timeout">Inactive, check app</string>
|
<string name="controls_error_timeout">Inactive, check app</string>
|
||||||
|
|||||||
@@ -17,12 +17,8 @@
|
|||||||
package com.android.systemui.media;
|
package com.android.systemui.media;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
@@ -35,7 +31,6 @@ import android.graphics.drawable.RippleDrawable;
|
|||||||
import android.media.session.MediaController;
|
import android.media.session.MediaController;
|
||||||
import android.media.session.MediaSession;
|
import android.media.session.MediaSession;
|
||||||
import android.media.session.PlaybackState;
|
import android.media.session.PlaybackState;
|
||||||
import android.service.media.MediaBrowserService;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -55,7 +50,6 @@ 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.util.animation.TransitionLayout;
|
import com.android.systemui.util.animation.TransitionLayout;
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
@@ -81,7 +75,6 @@ public class MediaControlPanel {
|
|||||||
|
|
||||||
private final SeekBarViewModel mSeekBarViewModel;
|
private final SeekBarViewModel mSeekBarViewModel;
|
||||||
private SeekBarObserver mSeekBarObserver;
|
private SeekBarObserver mSeekBarObserver;
|
||||||
private final Executor mForegroundExecutor;
|
|
||||||
protected final Executor mBackgroundExecutor;
|
protected final Executor mBackgroundExecutor;
|
||||||
private final ActivityStarter mActivityStarter;
|
private final ActivityStarter mActivityStarter;
|
||||||
|
|
||||||
@@ -91,48 +84,18 @@ public class MediaControlPanel {
|
|||||||
private MediaSession.Token mToken;
|
private MediaSession.Token mToken;
|
||||||
private MediaController mController;
|
private MediaController mController;
|
||||||
private int mBackgroundColor;
|
private int mBackgroundColor;
|
||||||
protected ComponentName mServiceComponent;
|
|
||||||
private boolean mIsRegistered = false;
|
|
||||||
private String mKey;
|
|
||||||
private int mAlbumArtSize;
|
private int mAlbumArtSize;
|
||||||
private int mAlbumArtRadius;
|
private int mAlbumArtRadius;
|
||||||
private int mViewWidth;
|
|
||||||
|
|
||||||
public static final String MEDIA_PREFERENCES = "media_control_prefs";
|
|
||||||
public static final String MEDIA_PREFERENCE_KEY = "browser_components";
|
|
||||||
private SharedPreferences mSharedPrefs;
|
|
||||||
private boolean mCheckedForResumption = false;
|
|
||||||
private QSMediaBrowser mQSMediaBrowser;
|
|
||||||
|
|
||||||
private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onSessionDestroyed() {
|
|
||||||
Log.d(TAG, "session destroyed");
|
|
||||||
mController.unregisterCallback(mSessionCallback);
|
|
||||||
clearControls();
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onPlaybackStateChanged(PlaybackState state) {
|
|
||||||
final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
|
|
||||||
if (s == PlaybackState.STATE_NONE) {
|
|
||||||
Log.d(TAG, "playback state change will trigger resumption, state=" + state);
|
|
||||||
clearControls();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a new control panel
|
* Initialize a new control panel
|
||||||
* @param context
|
* @param context
|
||||||
* @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, Executor foregroundExecutor,
|
public MediaControlPanel(Context context, DelayableExecutor backgroundExecutor,
|
||||||
DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
|
ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager) {
|
||||||
MediaHostStatesManager mediaHostStatesManager) {
|
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mForegroundExecutor = foregroundExecutor;
|
|
||||||
mBackgroundExecutor = backgroundExecutor;
|
mBackgroundExecutor = backgroundExecutor;
|
||||||
mActivityStarter = activityStarter;
|
mActivityStarter = activityStarter;
|
||||||
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
|
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
|
||||||
@@ -214,45 +177,18 @@ public class MediaControlPanel {
|
|||||||
MediaSession.Token token = data.getToken();
|
MediaSession.Token token = data.getToken();
|
||||||
mBackgroundColor = data.getBackgroundColor();
|
mBackgroundColor = data.getBackgroundColor();
|
||||||
if (mToken == null || !mToken.equals(token)) {
|
if (mToken == null || !mToken.equals(token)) {
|
||||||
if (mQSMediaBrowser != null) {
|
|
||||||
Log.d(TAG, "Disconnecting old media browser");
|
|
||||||
mQSMediaBrowser.disconnect();
|
|
||||||
mQSMediaBrowser = null;
|
|
||||||
}
|
|
||||||
mToken = token;
|
mToken = token;
|
||||||
mServiceComponent = null;
|
|
||||||
mCheckedForResumption = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mController = new MediaController(mContext, mToken);
|
if (mToken != null) {
|
||||||
|
mController = new MediaController(mContext, mToken);
|
||||||
|
} else {
|
||||||
|
mController = null;
|
||||||
|
}
|
||||||
|
|
||||||
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
||||||
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
||||||
|
|
||||||
// Try to find a browser service component for this app
|
|
||||||
// TODO also check for a media button receiver intended for restarting (b/154127084)
|
|
||||||
// Only check if we haven't tried yet or the session token changed
|
|
||||||
final String pkgName = data.getPackageName();
|
|
||||||
if (mServiceComponent == null && !mCheckedForResumption) {
|
|
||||||
Log.d(TAG, "Checking for service component");
|
|
||||||
PackageManager pm = mContext.getPackageManager();
|
|
||||||
Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
|
|
||||||
List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
|
|
||||||
// TODO: look into this resumption
|
|
||||||
if (resumeInfo != null) {
|
|
||||||
for (ResolveInfo inf : resumeInfo) {
|
|
||||||
if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
|
|
||||||
mBackgroundExecutor.execute(() ->
|
|
||||||
tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mCheckedForResumption = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
mController.registerCallback(mSessionCallback);
|
|
||||||
|
|
||||||
mViewHolder.getPlayer().setBackgroundTintList(
|
mViewHolder.getPlayer().setBackgroundTintList(
|
||||||
ColorStateList.valueOf(mBackgroundColor));
|
ColorStateList.valueOf(mBackgroundColor));
|
||||||
|
|
||||||
@@ -267,12 +203,22 @@ public class MediaControlPanel {
|
|||||||
ImageView albumView = mViewHolder.getAlbumView();
|
ImageView albumView = mViewHolder.getAlbumView();
|
||||||
// TODO: migrate this to a view with rounded corners instead of baking the rounding
|
// TODO: migrate this to a view with rounded corners instead of baking the rounding
|
||||||
// into the bitmap
|
// into the bitmap
|
||||||
Drawable artwork = createRoundedBitmap(data.getArtwork());
|
boolean hasArtwork = data.getArtwork() != null;
|
||||||
albumView.setImageDrawable(artwork);
|
if (hasArtwork) {
|
||||||
|
Drawable artwork = createRoundedBitmap(data.getArtwork());
|
||||||
|
albumView.setImageDrawable(artwork);
|
||||||
|
}
|
||||||
|
setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork);
|
||||||
|
setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);
|
||||||
|
|
||||||
// App icon
|
// App icon
|
||||||
ImageView appIcon = mViewHolder.getAppIcon();
|
ImageView appIcon = mViewHolder.getAppIcon();
|
||||||
appIcon.setImageDrawable(data.getAppIcon());
|
if (data.getAppIcon() != null) {
|
||||||
|
appIcon.setImageDrawable(data.getAppIcon());
|
||||||
|
} else {
|
||||||
|
Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note);
|
||||||
|
appIcon.setImageDrawable(iconDrawable);
|
||||||
|
}
|
||||||
|
|
||||||
// Song name
|
// Song name
|
||||||
TextView titleText = mViewHolder.getTitleText();
|
TextView titleText = mViewHolder.getTitleText();
|
||||||
@@ -294,7 +240,7 @@ public class MediaControlPanel {
|
|||||||
final Intent intent = new Intent()
|
final Intent intent = new Intent()
|
||||||
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
|
.setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
|
||||||
.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
|
.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
|
||||||
mController.getPackageName())
|
data.getPackageName())
|
||||||
.putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
|
.putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
|
||||||
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);
|
||||||
@@ -350,15 +296,11 @@ public class MediaControlPanel {
|
|||||||
MediaAction mediaAction = actionIcons.get(i);
|
MediaAction mediaAction = actionIcons.get(i);
|
||||||
button.setImageDrawable(mediaAction.getDrawable());
|
button.setImageDrawable(mediaAction.getDrawable());
|
||||||
button.setContentDescription(mediaAction.getContentDescription());
|
button.setContentDescription(mediaAction.getContentDescription());
|
||||||
PendingIntent actionIntent = mediaAction.getIntent();
|
Runnable action = mediaAction.getAction();
|
||||||
|
|
||||||
button.setOnClickListener(v -> {
|
button.setOnClickListener(v -> {
|
||||||
if (actionIntent != null) {
|
if (action != null) {
|
||||||
try {
|
action.run();
|
||||||
actionIntent.send();
|
|
||||||
} catch (PendingIntent.CanceledException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
|
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
|
||||||
@@ -443,14 +385,6 @@ public class MediaControlPanel {
|
|||||||
return mController.getPackageName();
|
return mController.getPackageName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the original notification's key
|
|
||||||
* @return The notification key
|
|
||||||
*/
|
|
||||||
public String getKey() {
|
|
||||||
return mKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether this player has an attached media session.
|
* Check whether this player has an attached media session.
|
||||||
* @return whether there is a controller with a current media session.
|
* @return whether there is a controller with a current media session.
|
||||||
@@ -485,150 +419,8 @@ public class MediaControlPanel {
|
|||||||
return (state.getState() == PlaybackState.STATE_PLAYING);
|
return (state.getState() == PlaybackState.STATE_PLAYING);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts controls into a resumption state if possible, or calls removePlayer if no component was
|
|
||||||
* found that could resume playback
|
|
||||||
*/
|
|
||||||
public void clearControls() {
|
|
||||||
Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
|
|
||||||
if (mServiceComponent == null) {
|
|
||||||
// If we don't have a way to resume, just remove the player altogether
|
|
||||||
Log.d(TAG, "Removing unresumable controls");
|
|
||||||
removePlayer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the media buttons and show only a restart button
|
|
||||||
*/
|
|
||||||
protected void resetButtons() {
|
|
||||||
if (mViewHolder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Hide all the old buttons
|
|
||||||
|
|
||||||
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
|
||||||
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
|
||||||
for (int i = 1; i < ACTION_IDS.length; i++) {
|
|
||||||
setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
|
|
||||||
setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a restart button
|
|
||||||
ImageButton btn = mViewHolder.getAction0();
|
|
||||||
btn.setOnClickListener(v -> {
|
|
||||||
Log.d(TAG, "Attempting to restart session");
|
|
||||||
if (mQSMediaBrowser != null) {
|
|
||||||
mQSMediaBrowser.disconnect();
|
|
||||||
}
|
|
||||||
mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
|
|
||||||
@Override
|
|
||||||
public void onConnected() {
|
|
||||||
Log.d(TAG, "Successfully restarted");
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onError() {
|
|
||||||
Log.e(TAG, "Error restarting");
|
|
||||||
mQSMediaBrowser.disconnect();
|
|
||||||
mQSMediaBrowser = null;
|
|
||||||
}
|
|
||||||
}, mServiceComponent);
|
|
||||||
mQSMediaBrowser.restart();
|
|
||||||
});
|
|
||||||
btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
|
|
||||||
setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */);
|
|
||||||
setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */);
|
|
||||||
|
|
||||||
mSeekBarViewModel.clearController();
|
|
||||||
// TODO: fix guts
|
|
||||||
// View guts = mMediaNotifView.findViewById(R.id.media_guts);
|
|
||||||
View options = mViewHolder.getOptions();
|
|
||||||
|
|
||||||
mViewHolder.getPlayer().setOnLongClickListener(v -> {
|
|
||||||
// Replace player view with close/cancel view
|
|
||||||
// guts.setVisibility(View.GONE);
|
|
||||||
options.setVisibility(View.VISIBLE);
|
|
||||||
return true; // consumed click
|
|
||||||
});
|
|
||||||
mMediaViewController.refreshState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
|
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
|
||||||
set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
|
set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
|
||||||
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
|
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that we can connect to the given component with a MediaBrowser, and if so, add that
|
|
||||||
* component to the list of resumption components
|
|
||||||
*/
|
|
||||||
private void tryUpdateResumptionList(ComponentName componentName) {
|
|
||||||
Log.d(TAG, "Testing if we can connect to " + componentName);
|
|
||||||
if (mQSMediaBrowser != null) {
|
|
||||||
mQSMediaBrowser.disconnect();
|
|
||||||
}
|
|
||||||
mQSMediaBrowser = new QSMediaBrowser(mContext,
|
|
||||||
new QSMediaBrowser.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onConnected() {
|
|
||||||
Log.d(TAG, "yes we can resume with " + componentName);
|
|
||||||
mServiceComponent = componentName;
|
|
||||||
updateResumptionList(componentName);
|
|
||||||
mQSMediaBrowser.disconnect();
|
|
||||||
mQSMediaBrowser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError() {
|
|
||||||
Log.d(TAG, "Cannot resume with " + componentName);
|
|
||||||
mServiceComponent = null;
|
|
||||||
if (!hasMediaSession()) {
|
|
||||||
// If it's not active and we can't resume, remove
|
|
||||||
removePlayer();
|
|
||||||
}
|
|
||||||
mQSMediaBrowser.disconnect();
|
|
||||||
mQSMediaBrowser = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentName);
|
|
||||||
mQSMediaBrowser.testConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the component to the saved list of media browser services, checking for duplicates and
|
|
||||||
* removing older components that exceed the maximum limit
|
|
||||||
* @param componentName
|
|
||||||
*/
|
|
||||||
private synchronized void updateResumptionList(ComponentName componentName) {
|
|
||||||
// Add to front of saved list
|
|
||||||
if (mSharedPrefs == null) {
|
|
||||||
mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
|
|
||||||
}
|
|
||||||
String componentString = componentName.flattenToString();
|
|
||||||
String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
|
|
||||||
if (listString == null) {
|
|
||||||
listString = componentString;
|
|
||||||
} else {
|
|
||||||
String[] components = listString.split(QSMediaBrowser.DELIMITER);
|
|
||||||
StringBuilder updated = new StringBuilder(componentString);
|
|
||||||
int nBrowsers = 1;
|
|
||||||
for (int i = 0; i < components.length
|
|
||||||
&& nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
|
|
||||||
if (componentString.equals(components[i])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
|
|
||||||
nBrowsers++;
|
|
||||||
}
|
|
||||||
listString = updated.toString();
|
|
||||||
}
|
|
||||||
mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a player can't be resumed to give it an opportunity to hide or remove itself
|
|
||||||
*/
|
|
||||||
protected void removePlayer() { }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,19 @@ data class MediaData(
|
|||||||
val artwork: Icon?,
|
val artwork: Icon?,
|
||||||
val actions: List<MediaAction>,
|
val actions: List<MediaAction>,
|
||||||
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?,
|
val device: MediaDeviceData?,
|
||||||
val notificationKey: String = "INVALID"
|
var resumeAction: Runnable?,
|
||||||
|
val notificationKey: String = "INVALID",
|
||||||
|
var hasCheckedForResume: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
/** State of a media action. */
|
/** State of a media action. */
|
||||||
data class MediaAction(
|
data class MediaAction(
|
||||||
val drawable: Drawable?,
|
val drawable: Drawable?,
|
||||||
val intent: PendingIntent?,
|
val action: Runnable?,
|
||||||
val contentDescription: CharSequence?
|
val contentDescription: CharSequence?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,15 @@ class MediaDataCombineLatest @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
dataSource.addListener(object : MediaDataManager.Listener {
|
dataSource.addListener(object : MediaDataManager.Listener {
|
||||||
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
entries[key] = data to entries[key]?.second
|
if (oldKey != null && !oldKey.equals(key)) {
|
||||||
update(key)
|
val s = entries[oldKey]?.second
|
||||||
|
entries[key] = data to entries[oldKey]?.second
|
||||||
|
entries.remove(oldKey)
|
||||||
|
} else {
|
||||||
|
entries[key] = data to entries[key]?.second
|
||||||
|
}
|
||||||
|
update(key, oldKey)
|
||||||
}
|
}
|
||||||
override fun onMediaDataRemoved(key: String) {
|
override fun onMediaDataRemoved(key: String) {
|
||||||
remove(key)
|
remove(key)
|
||||||
@@ -43,7 +49,7 @@ class MediaDataCombineLatest @Inject constructor(
|
|||||||
deviceSource.addListener(object : MediaDeviceManager.Listener {
|
deviceSource.addListener(object : MediaDeviceManager.Listener {
|
||||||
override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) {
|
override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) {
|
||||||
entries[key] = entries[key]?.first to data
|
entries[key] = entries[key]?.first to data
|
||||||
update(key)
|
update(key, key)
|
||||||
}
|
}
|
||||||
override fun onKeyRemoved(key: String) {
|
override fun onKeyRemoved(key: String) {
|
||||||
remove(key)
|
remove(key)
|
||||||
@@ -61,13 +67,13 @@ class MediaDataCombineLatest @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
|
fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
|
||||||
|
|
||||||
private fun update(key: String) {
|
private fun update(key: String, oldKey: String?) {
|
||||||
val (entry, device) = entries[key] ?: null to null
|
val (entry, device) = entries[key] ?: null to null
|
||||||
if (entry != null && device != null) {
|
if (entry != null && device != null) {
|
||||||
val data = entry.copy(device = device)
|
val data = entry.copy(device = device)
|
||||||
val listenersCopy = listeners.toSet()
|
val listenersCopy = listeners.toSet()
|
||||||
listenersCopy.forEach {
|
listenersCopy.forEach {
|
||||||
it.onMediaDataLoaded(key, data)
|
it.onMediaDataLoaded(key, oldKey, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package com.android.systemui.media
|
package com.android.systemui.media
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -25,6 +26,7 @@ import android.graphics.Color
|
|||||||
import android.graphics.ImageDecoder
|
import android.graphics.ImageDecoder
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
|
import android.media.MediaDescription
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.media.session.MediaSession
|
import android.media.session.MediaSession
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -32,8 +34,10 @@ import android.service.notification.StatusBarNotification
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.internal.graphics.ColorUtils
|
import com.android.internal.graphics.ColorUtils
|
||||||
|
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
|
||||||
|
import com.android.systemui.statusbar.NotificationMediaManager
|
||||||
import com.android.systemui.statusbar.notification.MediaNotificationProcessor
|
import com.android.systemui.statusbar.notification.MediaNotificationProcessor
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager
|
import com.android.systemui.statusbar.notification.NotificationEntryManager
|
||||||
import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON
|
import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON
|
||||||
@@ -58,7 +62,7 @@ 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, null)
|
emptyList(), emptyList(), "INVALID", null, null, null, null)
|
||||||
|
|
||||||
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||||
if (!sbn.notification.hasMediaSession()) {
|
if (!sbn.notification.hasMediaSession()) {
|
||||||
@@ -81,34 +85,92 @@ class MediaDataManager @Inject constructor(
|
|||||||
private val mediaControllerFactory: MediaControllerFactory,
|
private val mediaControllerFactory: MediaControllerFactory,
|
||||||
private val mediaTimeoutListener: MediaTimeoutListener,
|
private val mediaTimeoutListener: MediaTimeoutListener,
|
||||||
private val notificationEntryManager: NotificationEntryManager,
|
private val notificationEntryManager: NotificationEntryManager,
|
||||||
|
private val mediaResumeListener: MediaResumeListener,
|
||||||
@Background private val backgroundExecutor: Executor,
|
@Background private val backgroundExecutor: Executor,
|
||||||
@Main private val foregroundExecutor: Executor
|
@Main private val foregroundExecutor: Executor
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val listeners: MutableSet<Listener> = mutableSetOf()
|
private val listeners: MutableSet<Listener> = mutableSetOf()
|
||||||
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||||
|
private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
|
mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
|
||||||
setTimedOut(token, timedOut) }
|
setTimedOut(token, timedOut) }
|
||||||
addListener(mediaTimeoutListener)
|
addListener(mediaTimeoutListener)
|
||||||
|
|
||||||
|
if (useMediaResumption) {
|
||||||
|
mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
|
||||||
|
resumeAction: Runnable, token: MediaSession.Token, appName: String,
|
||||||
|
appIntent: PendingIntent, packageName: String ->
|
||||||
|
addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName)
|
||||||
|
}
|
||||||
|
mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? ->
|
||||||
|
mediaEntries.get(key)?.resumeAction = action
|
||||||
|
mediaEntries.get(key)?.hasCheckedForResume = true
|
||||||
|
}
|
||||||
|
addListener(mediaResumeListener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
||||||
if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
|
if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
|
||||||
Assert.isMainThread()
|
Assert.isMainThread()
|
||||||
if (!mediaEntries.containsKey(key)) {
|
val oldKey = findExistingEntry(key, sbn.packageName)
|
||||||
mediaEntries.put(key, LOADING)
|
if (oldKey == null) {
|
||||||
|
val temp = LOADING.copy(packageName = sbn.packageName)
|
||||||
|
mediaEntries.put(key, temp)
|
||||||
|
} else if (oldKey != key) {
|
||||||
|
// Move to new key
|
||||||
|
val oldData = mediaEntries.remove(oldKey)!!
|
||||||
|
mediaEntries.put(key, oldData)
|
||||||
}
|
}
|
||||||
loadMediaData(key, sbn)
|
loadMediaData(key, sbn, oldKey)
|
||||||
} else {
|
} else {
|
||||||
onNotificationRemoved(key)
|
onNotificationRemoved(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMediaData(key: String, sbn: StatusBarNotification) {
|
private fun addResumptionControls(
|
||||||
|
desc: MediaDescription,
|
||||||
|
action: Runnable,
|
||||||
|
token: MediaSession.Token,
|
||||||
|
appName: String,
|
||||||
|
appIntent: PendingIntent,
|
||||||
|
packageName: String
|
||||||
|
) {
|
||||||
|
// Resume controls don't have a notification key, so store by package name instead
|
||||||
|
if (!mediaEntries.containsKey(packageName)) {
|
||||||
|
val resumeData = LOADING.copy(packageName = packageName, resumeAction = action)
|
||||||
|
mediaEntries.put(packageName, resumeData)
|
||||||
|
}
|
||||||
backgroundExecutor.execute {
|
backgroundExecutor.execute {
|
||||||
loadMediaDataInBg(key, sbn)
|
loadMediaDataInBg(desc, action, token, appName, appIntent, packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is an existing entry that matches the key or package name.
|
||||||
|
* Returns the key that matches, or null if not found.
|
||||||
|
*/
|
||||||
|
private fun findExistingEntry(key: String, packageName: String): String? {
|
||||||
|
if (mediaEntries.containsKey(key)) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
// Check if we already had a resume player
|
||||||
|
if (mediaEntries.containsKey(packageName)) {
|
||||||
|
return packageName
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMediaData(
|
||||||
|
key: String,
|
||||||
|
sbn: StatusBarNotification,
|
||||||
|
oldKey: String?
|
||||||
|
) {
|
||||||
|
backgroundExecutor.execute {
|
||||||
|
loadMediaDataInBg(key, sbn, oldKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +194,50 @@ class MediaDataManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
|
private fun loadMediaDataInBg(
|
||||||
|
desc: MediaDescription,
|
||||||
|
resumeAction: Runnable,
|
||||||
|
token: MediaSession.Token,
|
||||||
|
appName: String,
|
||||||
|
appIntent: PendingIntent,
|
||||||
|
packageName: String
|
||||||
|
) {
|
||||||
|
if (resumeAction == null) {
|
||||||
|
Log.e(TAG, "Resume action cannot be null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(desc.title)) {
|
||||||
|
Log.e(TAG, "Description incomplete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "adding track from browser: $desc")
|
||||||
|
|
||||||
|
// Album art
|
||||||
|
var artworkBitmap = desc.iconBitmap
|
||||||
|
if (artworkBitmap == null && desc.iconUri != null) {
|
||||||
|
artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
|
||||||
|
}
|
||||||
|
val artworkIcon = if (artworkBitmap != null) {
|
||||||
|
Icon.createWithBitmap(artworkBitmap)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaAction = getResumeMediaAction(resumeAction)
|
||||||
|
foregroundExecutor.execute {
|
||||||
|
onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName,
|
||||||
|
null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
|
||||||
|
packageName, token, appIntent, null, resumeAction, packageName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMediaDataInBg(
|
||||||
|
key: String,
|
||||||
|
sbn: StatusBarNotification,
|
||||||
|
oldKey: String?
|
||||||
|
) {
|
||||||
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
|
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
|
||||||
as MediaSession.Token?
|
as MediaSession.Token?
|
||||||
val metadata = mediaControllerFactory.create(token).metadata
|
val metadata = mediaControllerFactory.create(token).metadata
|
||||||
@@ -234,16 +339,23 @@ class MediaDataManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
val mediaAction = MediaAction(
|
val mediaAction = MediaAction(
|
||||||
action.getIcon().loadDrawable(packageContext),
|
action.getIcon().loadDrawable(packageContext),
|
||||||
action.actionIntent,
|
Runnable {
|
||||||
|
try {
|
||||||
|
action.actionIntent.send()
|
||||||
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
|
Log.d(TAG, "Intent canceled", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
action.title)
|
action.title)
|
||||||
actionIcons.add(mediaAction)
|
actionIcons.add(mediaAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction
|
||||||
foregroundExecutor.execute {
|
foregroundExecutor.execute {
|
||||||
onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
|
onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
|
||||||
artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
|
||||||
notif.contentIntent, null, key))
|
notif.contentIntent, null, resumeAction, key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +369,7 @@ class MediaDataManager @Inject constructor(
|
|||||||
val albumArt = loadBitmapFromUri(Uri.parse(uriString))
|
val albumArt = loadBitmapFromUri(Uri.parse(uriString))
|
||||||
if (albumArt != null) {
|
if (albumArt != null) {
|
||||||
Log.d(TAG, "loaded art from $uri")
|
Log.d(TAG, "loaded art from $uri")
|
||||||
break
|
return albumArt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,27 +395,52 @@ class MediaDataManager @Inject constructor(
|
|||||||
|
|
||||||
val source = ImageDecoder.createSource(context.getContentResolver(), uri)
|
val source = ImageDecoder.createSource(context.getContentResolver(), uri)
|
||||||
return try {
|
return try {
|
||||||
ImageDecoder.decodeBitmap(source)
|
ImageDecoder.decodeBitmap(source) {
|
||||||
|
decoder, info, source -> decoder.isMutableRequired = true
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMediaDataLoaded(key: String, data: MediaData) {
|
private fun getResumeMediaAction(action: Runnable): MediaAction {
|
||||||
|
return MediaAction(
|
||||||
|
context.getDrawable(R.drawable.lb_ic_play),
|
||||||
|
action,
|
||||||
|
context.getString(R.string.controls_media_resume)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
Assert.isMainThread()
|
Assert.isMainThread()
|
||||||
if (mediaEntries.containsKey(key)) {
|
if (mediaEntries.containsKey(key)) {
|
||||||
// Otherwise this was removed already
|
// Otherwise this was removed already
|
||||||
mediaEntries.put(key, data)
|
mediaEntries.put(key, data)
|
||||||
val listenersCopy = listeners.toSet()
|
val listenersCopy = listeners.toSet()
|
||||||
listenersCopy.forEach {
|
listenersCopy.forEach {
|
||||||
it.onMediaDataLoaded(key, data)
|
it.onMediaDataLoaded(key, oldKey, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNotificationRemoved(key: String) {
|
fun onNotificationRemoved(key: String) {
|
||||||
Assert.isMainThread()
|
Assert.isMainThread()
|
||||||
|
if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) {
|
||||||
|
Log.d(TAG, "Not removing $key because resumable")
|
||||||
|
// Move to resume key aka package name
|
||||||
|
val data = mediaEntries.remove(key)!!
|
||||||
|
val resumeAction = getResumeMediaAction(data.resumeAction!!)
|
||||||
|
val updated = data.copy(token = null, actions = listOf(resumeAction),
|
||||||
|
actionsToShowInCompact = listOf(0))
|
||||||
|
mediaEntries.put(data.packageName, updated)
|
||||||
|
// Notify listeners of "new" controls
|
||||||
|
val listenersCopy = listeners.toSet()
|
||||||
|
listenersCopy.forEach {
|
||||||
|
it.onMediaDataLoaded(data.packageName, key, updated)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
val removed = mediaEntries.remove(key)
|
val removed = mediaEntries.remove(key)
|
||||||
if (removed != null) {
|
if (removed != null) {
|
||||||
val listenersCopy = listeners.toSet()
|
val listenersCopy = listeners.toSet()
|
||||||
@@ -316,19 +453,32 @@ class MediaDataManager @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Are there any media notifications active?
|
* Are there any media notifications active?
|
||||||
*/
|
*/
|
||||||
fun hasActiveMedia() = mediaEntries.isNotEmpty()
|
fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) })
|
||||||
|
|
||||||
fun hasAnyMedia(): Boolean {
|
fun isActive(data: MediaData): Boolean {
|
||||||
// TODO: implement this when we implemented resumption
|
if (data.token == null) {
|
||||||
return hasActiveMedia()
|
return false
|
||||||
|
}
|
||||||
|
val controller = mediaControllerFactory.create(data.token)
|
||||||
|
val state = controller?.playbackState?.state
|
||||||
|
return state != null && NotificationMediaManager.isActiveState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are there any media entries, including resume controls?
|
||||||
|
*/
|
||||||
|
fun hasAnyMedia() = mediaEntries.isNotEmpty()
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called whenever there's new MediaData Loaded for the consumption in views
|
* Called whenever there's new MediaData Loaded for the consumption in views.
|
||||||
|
*
|
||||||
|
* oldKey is provided to check whether the view has changed keys, which can happen when a
|
||||||
|
* player has gone from resume state (key is package name) to active state (key is
|
||||||
|
* notification key) or vice versa.
|
||||||
*/
|
*/
|
||||||
fun onMediaDataLoaded(key: String, data: MediaData) {}
|
fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called whenever a previously existing Media notification was removed
|
* Called whenever a previously existing Media notification was removed
|
||||||
|
|||||||
@@ -16,11 +16,8 @@
|
|||||||
|
|
||||||
package com.android.systemui.media
|
package com.android.systemui.media
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.service.notification.StatusBarNotification
|
|
||||||
import android.media.MediaRouter2Manager
|
import android.media.MediaRouter2Manager
|
||||||
import android.media.session.MediaSession
|
|
||||||
import android.media.session.MediaController
|
import android.media.session.MediaController
|
||||||
import com.android.settingslib.media.LocalMediaManager
|
import com.android.settingslib.media.LocalMediaManager
|
||||||
import com.android.settingslib.media.MediaDevice
|
import com.android.settingslib.media.MediaDevice
|
||||||
@@ -38,11 +35,16 @@ class MediaDeviceManager @Inject constructor(
|
|||||||
private val localMediaManagerFactory: LocalMediaManagerFactory,
|
private val localMediaManagerFactory: LocalMediaManagerFactory,
|
||||||
private val mr2manager: MediaRouter2Manager,
|
private val mr2manager: MediaRouter2Manager,
|
||||||
private val featureFlag: MediaFeatureFlag,
|
private val featureFlag: MediaFeatureFlag,
|
||||||
@Main private val fgExecutor: Executor
|
@Main private val fgExecutor: Executor,
|
||||||
) {
|
private val mediaDataManager: MediaDataManager
|
||||||
|
) : MediaDataManager.Listener {
|
||||||
private val listeners: MutableSet<Listener> = mutableSetOf()
|
private val listeners: MutableSet<Listener> = mutableSetOf()
|
||||||
private val entries: MutableMap<String, Token> = mutableMapOf()
|
private val entries: MutableMap<String, Token> = mutableMapOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
mediaDataManager.addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener for changes to the media route (ie. device).
|
* Add a listener for changes to the media route (ie. device).
|
||||||
*/
|
*/
|
||||||
@@ -53,23 +55,25 @@ class MediaDeviceManager @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
fun removeListener(listener: Listener) = listeners.remove(listener)
|
fun removeListener(listener: Listener) = listeners.remove(listener)
|
||||||
|
|
||||||
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
|
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
if (featureFlag.enabled && isMediaNotification(sbn)) {
|
if (featureFlag.enabled) {
|
||||||
|
if (oldKey != null && oldKey != key) {
|
||||||
|
val oldToken = entries.remove(oldKey)
|
||||||
|
oldToken?.stop()
|
||||||
|
}
|
||||||
var tok = entries[key]
|
var tok = entries[key]
|
||||||
if (tok == null) {
|
if (tok == null && data.token != null) {
|
||||||
val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
|
val controller = MediaController(context, data.token!!)
|
||||||
as MediaSession.Token?
|
tok = Token(key, controller, localMediaManagerFactory.create(data.packageName))
|
||||||
val controller = MediaController(context, token)
|
|
||||||
tok = Token(key, controller, localMediaManagerFactory.create(sbn.packageName))
|
|
||||||
entries[key] = tok
|
entries[key] = tok
|
||||||
tok.start()
|
tok.start()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onNotificationRemoved(key)
|
onMediaDataRemoved(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNotificationRemoved(key: String) {
|
override fun onMediaDataRemoved(key: String) {
|
||||||
val token = entries.remove(key)
|
val token = entries.remove(key)
|
||||||
token?.stop()
|
token?.stop()
|
||||||
token?.let {
|
token?.let {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class MediaHost @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val listener = object : MediaDataManager.Listener {
|
private val listener = object : MediaDataManager.Listener {
|
||||||
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
updateViewVisibility()
|
updateViewVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* 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.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.MediaDescription
|
||||||
|
import android.media.session.MediaController
|
||||||
|
import android.media.session.MediaSession
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.service.media.MediaBrowserService
|
||||||
|
import android.util.Log
|
||||||
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
|
import com.android.systemui.util.Utils
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private const val TAG = "MediaResumeListener"
|
||||||
|
|
||||||
|
private const val MEDIA_PREFERENCES = "media_control_prefs"
|
||||||
|
private const val MEDIA_PREFERENCE_KEY = "browser_components"
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class MediaResumeListener @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val broadcastDispatcher: BroadcastDispatcher,
|
||||||
|
@Background private val backgroundExecutor: Executor
|
||||||
|
) : MediaDataManager.Listener {
|
||||||
|
|
||||||
|
private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
|
||||||
|
private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
lateinit var addTrackToResumeCallback: (
|
||||||
|
MediaDescription,
|
||||||
|
Runnable,
|
||||||
|
MediaSession.Token,
|
||||||
|
String,
|
||||||
|
PendingIntent,
|
||||||
|
String
|
||||||
|
) -> Unit
|
||||||
|
lateinit var resumeComponentFoundCallback: (String, Runnable?) -> Unit
|
||||||
|
|
||||||
|
private var mediaBrowser: ResumeMediaBrowser? = null
|
||||||
|
|
||||||
|
private val unlockReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (Intent.ACTION_USER_UNLOCKED == intent.action) {
|
||||||
|
loadMediaResumptionControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
|
||||||
|
override fun addTrack(
|
||||||
|
desc: MediaDescription,
|
||||||
|
component: ComponentName,
|
||||||
|
browser: ResumeMediaBrowser
|
||||||
|
) {
|
||||||
|
val token = browser.token
|
||||||
|
val appIntent = browser.appIntent
|
||||||
|
val pm = context.getPackageManager()
|
||||||
|
var appName: CharSequence = component.packageName
|
||||||
|
val resumeAction = getResumeAction(component)
|
||||||
|
try {
|
||||||
|
appName = pm.getApplicationLabel(
|
||||||
|
pm.getApplicationInfo(component.packageName, 0))
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Log.e(TAG, "Error getting package information", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Adding resume controls $desc")
|
||||||
|
addTrackToResumeCallback(desc, resumeAction, token, appName.toString(), appIntent,
|
||||||
|
component.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (useMediaResumption) {
|
||||||
|
val unlockFilter = IntentFilter()
|
||||||
|
unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
|
||||||
|
broadcastDispatcher.registerReceiver(unlockReceiver, unlockFilter, null, UserHandle.ALL)
|
||||||
|
loadSavedComponents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSavedComponents() {
|
||||||
|
val userContext = context.createContextAsUser(context.getUser(), 0)
|
||||||
|
val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
|
||||||
|
val listString = prefs.getString(MEDIA_PREFERENCE_KEY, null)
|
||||||
|
val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
|
||||||
|
?.dropLastWhile { it.isEmpty() }
|
||||||
|
components?.forEach {
|
||||||
|
val info = it.split("/")
|
||||||
|
val packageName = info[0]
|
||||||
|
val className = info[1]
|
||||||
|
val component = ComponentName(packageName, className)
|
||||||
|
resumeComponents.add(component)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load controls for resuming media, if available
|
||||||
|
*/
|
||||||
|
private fun loadMediaResumptionControls() {
|
||||||
|
if (!useMediaResumption) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeComponents.forEach {
|
||||||
|
val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it)
|
||||||
|
browser.findRecentMedia()
|
||||||
|
}
|
||||||
|
broadcastDispatcher.unregisterReceiver(unlockReceiver) // only need to load once
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
|
if (useMediaResumption) {
|
||||||
|
// If this had been started from a resume state, disconnect now that it's live
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
// If we don't have a resume action, check if we haven't already
|
||||||
|
if (data.resumeAction == null && !data.hasCheckedForResume) {
|
||||||
|
// TODO also check for a media button receiver intended for restarting (b/154127084)
|
||||||
|
Log.d(TAG, "Checking for service component for " + data.packageName)
|
||||||
|
val pm = context.packageManager
|
||||||
|
val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
|
||||||
|
val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
|
||||||
|
|
||||||
|
val inf = resumeInfo?.filter {
|
||||||
|
it.serviceInfo.packageName == data.packageName
|
||||||
|
}
|
||||||
|
if (inf != null && inf.size > 0) {
|
||||||
|
backgroundExecutor.execute {
|
||||||
|
tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No service found
|
||||||
|
resumeComponentFoundCallback(key, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that we can connect to the given component with a MediaBrowser, and if so, add that
|
||||||
|
* component to the list of resumption components
|
||||||
|
*/
|
||||||
|
private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
|
||||||
|
Log.d(TAG, "Testing if we can connect to $componentName")
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
mediaBrowser = ResumeMediaBrowser(context,
|
||||||
|
object : ResumeMediaBrowser.Callback() {
|
||||||
|
override fun onConnected() {
|
||||||
|
Log.d(TAG, "yes we can resume with $componentName")
|
||||||
|
resumeComponentFoundCallback(key, getResumeAction(componentName))
|
||||||
|
updateResumptionList(componentName)
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
mediaBrowser = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError() {
|
||||||
|
Log.e(TAG, "Cannot resume with $componentName")
|
||||||
|
resumeComponentFoundCallback(key, null)
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
mediaBrowser = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentName)
|
||||||
|
mediaBrowser?.testConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the component to the saved list of media browser services, checking for duplicates and
|
||||||
|
* removing older components that exceed the maximum limit
|
||||||
|
* @param componentName
|
||||||
|
*/
|
||||||
|
private fun updateResumptionList(componentName: ComponentName) {
|
||||||
|
// Remove if exists
|
||||||
|
resumeComponents.remove(componentName)
|
||||||
|
// Insert at front of queue
|
||||||
|
resumeComponents.add(componentName)
|
||||||
|
// Remove old components if over the limit
|
||||||
|
if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
|
||||||
|
resumeComponents.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
val sb = StringBuilder()
|
||||||
|
resumeComponents.forEach {
|
||||||
|
sb.append(it.flattenToString())
|
||||||
|
sb.append(ResumeMediaBrowser.DELIMITER)
|
||||||
|
}
|
||||||
|
val userContext = context.createContextAsUser(context.getUser(), 0)
|
||||||
|
val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putString(MEDIA_PREFERENCE_KEY, sb.toString()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a runnable which will resume media playback
|
||||||
|
*/
|
||||||
|
private fun getResumeAction(componentName: ComponentName): Runnable {
|
||||||
|
return Runnable {
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
mediaBrowser = ResumeMediaBrowser(context,
|
||||||
|
object : ResumeMediaBrowser.Callback() {
|
||||||
|
override fun onConnected() {
|
||||||
|
if (mediaBrowser?.token == null) {
|
||||||
|
Log.e(TAG, "Error after connect")
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
mediaBrowser = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Connected for restart $componentName")
|
||||||
|
val controller = MediaController(context, mediaBrowser!!.token)
|
||||||
|
val controls = controller.transportControls
|
||||||
|
controls.prepare()
|
||||||
|
controls.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError() {
|
||||||
|
Log.e(TAG, "Resume failed for $componentName")
|
||||||
|
mediaBrowser?.disconnect()
|
||||||
|
mediaBrowser = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentName)
|
||||||
|
mediaBrowser?.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
|
|
||||||
lateinit var timeoutCallback: (String, Boolean) -> Unit
|
lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||||
|
|
||||||
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
if (mediaListeners.containsKey(key)) {
|
if (mediaListeners.containsKey(key)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,15 +67,20 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
|
|
||||||
var timedOut = false
|
var timedOut = false
|
||||||
|
|
||||||
private val mediaController = mediaControllerFactory.create(data.token)
|
// Resume controls may have null token
|
||||||
|
private val mediaController = if (data.token != null) {
|
||||||
|
mediaControllerFactory.create(data.token)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
private var cancellation: Runnable? = null
|
private var cancellation: Runnable? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mediaController.registerCallback(this)
|
mediaController?.registerCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
mediaController.unregisterCallback(this)
|
mediaController?.unregisterCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(state: PlaybackState?) {
|
override fun onPlaybackStateChanged(state: PlaybackState?) {
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ 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.dagger.qualifiers.Background
|
||||||
import com.android.systemui.dagger.qualifiers.Main
|
|
||||||
import com.android.systemui.plugins.ActivityStarter
|
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 com.android.systemui.util.concurrency.DelayableExecutor
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -32,7 +30,6 @@ private const val FLING_SLOP = 1000000
|
|||||||
@Singleton
|
@Singleton
|
||||||
class MediaViewManager @Inject constructor(
|
class MediaViewManager @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@Main private val foregroundExecutor: Executor,
|
|
||||||
@Background private val backgroundExecutor: DelayableExecutor,
|
@Background private val backgroundExecutor: DelayableExecutor,
|
||||||
private val visualStabilityManager: VisualStabilityManager,
|
private val visualStabilityManager: VisualStabilityManager,
|
||||||
private val activityStarter: ActivityStarter,
|
private val activityStarter: ActivityStarter,
|
||||||
@@ -147,8 +144,8 @@ class MediaViewManager @Inject constructor(
|
|||||||
visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
|
visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
|
||||||
true /* persistent */)
|
true /* persistent */)
|
||||||
mediaManager.addListener(object : MediaDataManager.Listener {
|
mediaManager.addListener(object : MediaDataManager.Listener {
|
||||||
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
|
||||||
updateView(key, data)
|
updateView(key, oldKey, data)
|
||||||
updatePlayerVisibilities()
|
updatePlayerVisibilities()
|
||||||
mediaCarousel.requiresRemeasuring = true
|
mediaCarousel.requiresRemeasuring = true
|
||||||
}
|
}
|
||||||
@@ -259,11 +256,17 @@ class MediaViewManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateView(key: String, data: MediaData) {
|
private fun updateView(key: String, oldKey: String?, data: MediaData) {
|
||||||
|
// If the key was changed, update entry
|
||||||
|
val oldData = mediaPlayers[oldKey]
|
||||||
|
if (oldData != null) {
|
||||||
|
val oldData = mediaPlayers.remove(oldKey)
|
||||||
|
mediaPlayers.put(key, oldData!!)
|
||||||
|
}
|
||||||
var existingPlayer = mediaPlayers[key]
|
var existingPlayer = mediaPlayers[key]
|
||||||
if (existingPlayer == null) {
|
if (existingPlayer == null) {
|
||||||
existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
|
existingPlayer = MediaControlPanel(context, backgroundExecutor, activityStarter,
|
||||||
activityStarter, mediaHostStatesManager)
|
mediaHostStatesManager)
|
||||||
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
||||||
mediaContent))
|
mediaContent))
|
||||||
mediaPlayers[key] = existingPlayer
|
mediaPlayers[key] = existingPlayer
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.android.systemui.qs;
|
package com.android.systemui.media;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@@ -27,14 +27,17 @@ import android.media.session.MediaController;
|
|||||||
import android.media.session.MediaSession;
|
import android.media.session.MediaSession;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.service.media.MediaBrowserService;
|
import android.service.media.MediaBrowserService;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.android.systemui.util.Utils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media browser for managing resumption in QS media controls
|
* Media browser for managing resumption in media controls
|
||||||
*/
|
*/
|
||||||
public class QSMediaBrowser {
|
public class ResumeMediaBrowser {
|
||||||
|
|
||||||
/** Maximum number of controls to show on boot */
|
/** Maximum number of controls to show on boot */
|
||||||
public static final int MAX_RESUMPTION_CONTROLS = 5;
|
public static final int MAX_RESUMPTION_CONTROLS = 5;
|
||||||
@@ -42,7 +45,8 @@ public class QSMediaBrowser {
|
|||||||
/** Delimiter for saved component names */
|
/** Delimiter for saved component names */
|
||||||
public static final String DELIMITER = ":";
|
public static final String DELIMITER = ":";
|
||||||
|
|
||||||
private static final String TAG = "QSMediaBrowser";
|
private static final String TAG = "ResumeMediaBrowser";
|
||||||
|
private boolean mIsEnabled = false;
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final Callback mCallback;
|
private final Callback mCallback;
|
||||||
private MediaBrowser mMediaBrowser;
|
private MediaBrowser mMediaBrowser;
|
||||||
@@ -54,21 +58,25 @@ public class QSMediaBrowser {
|
|||||||
* @param callback used to report media items found
|
* @param callback used to report media items found
|
||||||
* @param componentName Component name of the MediaBrowserService this browser will connect to
|
* @param componentName Component name of the MediaBrowserService this browser will connect to
|
||||||
*/
|
*/
|
||||||
public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) {
|
public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) {
|
||||||
|
mIsEnabled = Utils.useMediaResumption(context);
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mCallback = callback;
|
mCallback = callback;
|
||||||
mComponentName = componentName;
|
mComponentName = componentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to the MediaBrowserService and looks for valid media. If a media item is returned
|
* Connects to the MediaBrowserService and looks for valid media. If a media item is returned,
|
||||||
* by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription.
|
* ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription.
|
||||||
* QSMediaBrowser.Callback#onConnected and QSMediaBrowser.Callback#onError will also be called
|
* ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be
|
||||||
* when the initial connection is successful, or an error occurs. Note that it is possible for
|
* called when the initial connection is successful, or an error occurs.
|
||||||
* the service to connect but for no playable tracks to be found later.
|
* Note that it is possible for the service to connect but for no playable tracks to be found.
|
||||||
* QSMediaBrowser#disconnect will be called automatically with this function.
|
* ResumeMediaBrowser#disconnect will be called automatically with this function.
|
||||||
*/
|
*/
|
||||||
public void findRecentMedia() {
|
public void findRecentMedia() {
|
||||||
|
if (!mIsEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Log.d(TAG, "Connecting to " + mComponentName);
|
Log.d(TAG, "Connecting to " + mComponentName);
|
||||||
disconnect();
|
disconnect();
|
||||||
Bundle rootHints = new Bundle();
|
Bundle rootHints = new Bundle();
|
||||||
@@ -86,7 +94,7 @@ public class QSMediaBrowser {
|
|||||||
public void onChildrenLoaded(String parentId,
|
public void onChildrenLoaded(String parentId,
|
||||||
List<MediaBrowser.MediaItem> children) {
|
List<MediaBrowser.MediaItem> children) {
|
||||||
if (children.size() == 0) {
|
if (children.size() == 0) {
|
||||||
Log.e(TAG, "No children found for " + mComponentName);
|
Log.d(TAG, "No children found for " + mComponentName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We ask apps to return a playable item as the first child when sending
|
// We ask apps to return a playable item as the first child when sending
|
||||||
@@ -94,23 +102,24 @@ public class QSMediaBrowser {
|
|||||||
MediaBrowser.MediaItem child = children.get(0);
|
MediaBrowser.MediaItem child = children.get(0);
|
||||||
MediaDescription desc = child.getDescription();
|
MediaDescription desc = child.getDescription();
|
||||||
if (child.isPlayable()) {
|
if (child.isPlayable()) {
|
||||||
mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this);
|
mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
|
||||||
|
ResumeMediaBrowser.this);
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Child found but not playable for " + mComponentName);
|
Log.d(TAG, "Child found but not playable for " + mComponentName);
|
||||||
}
|
}
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(String parentId) {
|
public void onError(String parentId) {
|
||||||
Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
|
Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
|
||||||
mCallback.onError();
|
mCallback.onError();
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(String parentId, Bundle options) {
|
public void onError(String parentId, Bundle options) {
|
||||||
Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId
|
Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId
|
||||||
+ ", options: " + options);
|
+ ", options: " + options);
|
||||||
mCallback.onError();
|
mCallback.onError();
|
||||||
disconnect();
|
disconnect();
|
||||||
@@ -149,7 +158,7 @@ public class QSMediaBrowser {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionFailed() {
|
public void onConnectionFailed() {
|
||||||
Log.e(TAG, "Connection failed for " + mComponentName);
|
Log.d(TAG, "Connection failed for " + mComponentName);
|
||||||
mCallback.onError();
|
mCallback.onError();
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
@@ -167,11 +176,15 @@ public class QSMediaBrowser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to the MediaBrowserService and starts playback. QSMediaBrowser.Callback#onError or
|
* Connects to the MediaBrowserService and starts playback.
|
||||||
* QSMediaBrowser.Callback#onConnected will be called depending on whether it was successful.
|
* ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
|
||||||
* QSMediaBrowser#disconnect should be called after this to ensure the connection is closed.
|
* depending on whether it was successful.
|
||||||
|
* ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
|
||||||
*/
|
*/
|
||||||
public void restart() {
|
public void restart() {
|
||||||
|
if (!mIsEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
disconnect();
|
disconnect();
|
||||||
Bundle rootHints = new Bundle();
|
Bundle rootHints = new Bundle();
|
||||||
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
|
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
|
||||||
@@ -224,18 +237,21 @@ public class QSMediaBrowser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
|
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
|
||||||
* QSMediaBrowser.Callback#onError or QSMediaBrowser.Callback#onConnected will be called
|
* ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
|
||||||
* depending on whether it was successful.
|
* depending on whether it was successful.
|
||||||
* QSMediaBrowser#disconnect should be called after this to ensure the connection is closed.
|
* ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
|
||||||
*/
|
*/
|
||||||
public void testConnection() {
|
public void testConnection() {
|
||||||
|
if (!mIsEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
disconnect();
|
disconnect();
|
||||||
final MediaBrowser.ConnectionCallback connectionCallback =
|
final MediaBrowser.ConnectionCallback connectionCallback =
|
||||||
new MediaBrowser.ConnectionCallback() {
|
new MediaBrowser.ConnectionCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onConnected() {
|
public void onConnected() {
|
||||||
Log.d(TAG, "connected");
|
Log.d(TAG, "connected");
|
||||||
if (mMediaBrowser.getRoot() == null) {
|
if (TextUtils.isEmpty(mMediaBrowser.getRoot())) {
|
||||||
mCallback.onError();
|
mCallback.onError();
|
||||||
} else {
|
} else {
|
||||||
mCallback.onConnected();
|
mCallback.onConnected();
|
||||||
@@ -264,7 +280,7 @@ public class QSMediaBrowser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to handle results from QSMediaBrowser
|
* Interface to handle results from ResumeMediaBrowser
|
||||||
*/
|
*/
|
||||||
public static class Callback {
|
public static class Callback {
|
||||||
/**
|
/**
|
||||||
@@ -286,7 +302,7 @@ public class QSMediaBrowser {
|
|||||||
* @param browser reference to the browser
|
* @param browser reference to the browser
|
||||||
*/
|
*/
|
||||||
public void addTrack(MediaDescription track, ComponentName component,
|
public void addTrack(MediaDescription track, ComponentName component,
|
||||||
QSMediaBrowser browser) {
|
ResumeMediaBrowser browser) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,8 +91,14 @@ class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
|
|||||||
playbackState = state
|
playbackState = state
|
||||||
if (shouldPollPlaybackPosition()) {
|
if (shouldPollPlaybackPosition()) {
|
||||||
checkPlaybackPosition()
|
checkPlaybackPosition()
|
||||||
|
} else if (PlaybackState.STATE_NONE.equals(playbackState)) {
|
||||||
|
clearController()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSessionDestroyed() {
|
||||||
|
clearController()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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. */
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import com.android.systemui.qs.customize.QSCustomizer;
|
|||||||
import com.android.systemui.qs.logging.QSLogger;
|
import com.android.systemui.qs.logging.QSLogger;
|
||||||
import com.android.systemui.tuner.TunerService;
|
import com.android.systemui.tuner.TunerService;
|
||||||
import com.android.systemui.tuner.TunerService.Tunable;
|
import com.android.systemui.tuner.TunerService.Tunable;
|
||||||
import com.android.systemui.util.Utils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -82,8 +81,7 @@ public class QuickQSPanel extends QSPanel {
|
|||||||
MediaHost mediaHost,
|
MediaHost mediaHost,
|
||||||
UiEventLogger uiEventLogger
|
UiEventLogger uiEventLogger
|
||||||
) {
|
) {
|
||||||
super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost,
|
super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger);
|
||||||
uiEventLogger);
|
|
||||||
if (mFooter != null) {
|
if (mFooter != null) {
|
||||||
removeView(mFooter.getView());
|
removeView(mFooter.getView());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ 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;
|
||||||
@@ -102,6 +101,12 @@ public class NotificationMediaManager implements Dumpable {
|
|||||||
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
|
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
|
||||||
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
|
PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
|
||||||
}
|
}
|
||||||
|
private static final HashSet<Integer> INACTIVE_MEDIA_STATES = new HashSet<>();
|
||||||
|
static {
|
||||||
|
INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_NONE);
|
||||||
|
INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
|
||||||
|
INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
private final NotificationEntryManager mEntryManager;
|
private final NotificationEntryManager mEntryManager;
|
||||||
private final MediaDataManager mMediaDataManager;
|
private final MediaDataManager mMediaDataManager;
|
||||||
@@ -190,8 +195,7 @@ 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;
|
||||||
@@ -212,13 +216,11 @@ 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
|
||||||
@@ -239,7 +241,6 @@ 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());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,10 +253,24 @@ public class NotificationMediaManager implements Dumpable {
|
|||||||
mPropertiesChangedListener);
|
mPropertiesChangedListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a state should be considered actively playing
|
||||||
|
* @param state a PlaybackState
|
||||||
|
* @return true if playing
|
||||||
|
*/
|
||||||
public static boolean isPlayingState(int state) {
|
public static boolean isPlayingState(int state) {
|
||||||
return !PAUSED_MEDIA_STATES.contains(state);
|
return !PAUSED_MEDIA_STATES.contains(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a state should be considered active (playing or paused)
|
||||||
|
* @param state a PlaybackState
|
||||||
|
* @return true if active
|
||||||
|
*/
|
||||||
|
public static boolean isActiveState(int state) {
|
||||||
|
return !INACTIVE_MEDIA_STATES.contains(state);
|
||||||
|
}
|
||||||
|
|
||||||
public void setUpWithPresenter(NotificationPresenter presenter) {
|
public void setUpWithPresenter(NotificationPresenter presenter) {
|
||||||
mPresenter = presenter;
|
mPresenter = presenter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ 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;
|
||||||
@@ -51,8 +50,6 @@ import com.android.systemui.tracing.ProtoTracer;
|
|||||||
import com.android.systemui.util.DeviceConfigProxy;
|
import com.android.systemui.util.DeviceConfigProxy;
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import dagger.Lazy;
|
import dagger.Lazy;
|
||||||
@@ -105,8 +102,7 @@ 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,
|
||||||
@@ -116,8 +112,7 @@ public interface StatusBarDependenciesModule {
|
|||||||
keyguardBypassController,
|
keyguardBypassController,
|
||||||
mainExecutor,
|
mainExecutor,
|
||||||
deviceConfigProxy,
|
deviceConfigProxy,
|
||||||
mediaDataManager,
|
mediaDataManager);
|
||||||
mediaDeviceManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
|
|||||||
@@ -133,4 +133,13 @@ public class Utils {
|
|||||||
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
|
Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
|
||||||
return flag > 0;
|
return flag > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow media resumption controls. Requires {@link #useQsMediaPlayer(Context)} to be enabled.
|
||||||
|
* Off by default, but can be enabled by setting to 1
|
||||||
|
*/
|
||||||
|
public static boolean useMediaResumption(Context context) {
|
||||||
|
int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_resumption", 0);
|
||||||
|
return useQsMediaPlayer(context) && flag > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
private lateinit var player: MediaControlPanel
|
private lateinit var player: MediaControlPanel
|
||||||
|
|
||||||
private lateinit var fgExecutor: FakeExecutor
|
|
||||||
private lateinit var bgExecutor: FakeExecutor
|
private lateinit var bgExecutor: FakeExecutor
|
||||||
@Mock private lateinit var activityStarter: ActivityStarter
|
@Mock private lateinit var activityStarter: ActivityStarter
|
||||||
|
|
||||||
@@ -97,14 +96,12 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
fgExecutor = FakeExecutor(FakeSystemClock())
|
|
||||||
bgExecutor = FakeExecutor(FakeSystemClock())
|
bgExecutor = FakeExecutor(FakeSystemClock())
|
||||||
|
|
||||||
activityStarter = mock(ActivityStarter::class.java)
|
activityStarter = mock(ActivityStarter::class.java)
|
||||||
mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
|
mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
|
||||||
|
|
||||||
player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter,
|
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager)
|
||||||
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)
|
holder = mock(PlayerViewHolder::class.java)
|
||||||
@@ -171,7 +168,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, device)
|
emptyList(), PACKAGE, null, null, device, null)
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(player.isPlaying()).isFalse()
|
assertThat(player.isPlaying()).isFalse()
|
||||||
}
|
}
|
||||||
@@ -180,7 +177,7 @@ 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, device)
|
emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
|
||||||
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)
|
||||||
@@ -191,7 +188,7 @@ 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, device)
|
emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
|
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
|
||||||
verify(view).setBackgroundTintList(list.capture())
|
verify(view).setBackgroundTintList(list.capture())
|
||||||
@@ -202,7 +199,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
fun bindDevice() {
|
fun bindDevice() {
|
||||||
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, device)
|
emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
||||||
assertThat(seamless.isEnabled()).isTrue()
|
assertThat(seamless.isEnabled()).isTrue()
|
||||||
@@ -212,7 +209,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
fun bindDisabledDevice() {
|
fun bindDisabledDevice() {
|
||||||
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, disabledDevice)
|
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, null)
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(seamless.isEnabled()).isFalse()
|
assertThat(seamless.isEnabled()).isFalse()
|
||||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||||
@@ -223,7 +220,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
|||||||
fun bindNullDevice() {
|
fun bindNullDevice() {
|
||||||
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, null)
|
emptyList(), PACKAGE, session.getSessionToken(), null, null, null)
|
||||||
player.bind(state)
|
player.bind(state)
|
||||||
assertThat(seamless.isEnabled()).isTrue()
|
assertThat(seamless.isEnabled()).isTrue()
|
||||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
|||||||
mManager.addListener(mListener);
|
mManager.addListener(mListener);
|
||||||
|
|
||||||
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
|
||||||
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, KEY);
|
new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, null, KEY, false);
|
||||||
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void eventNotEmittedWithoutDevice() {
|
public void eventNotEmittedWithoutDevice() {
|
||||||
// WHEN data source emits an event without device data
|
// WHEN data source emits an event without device data
|
||||||
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
|
||||||
// THEN an event isn't emitted
|
// THEN an event isn't emitted
|
||||||
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
|
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -96,7 +96,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
|||||||
// WHEN device source emits an event without media data
|
// WHEN device source emits an event without media data
|
||||||
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
// THEN an event isn't emitted
|
// THEN an event isn't emitted
|
||||||
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
|
verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -104,22 +104,22 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
|||||||
// GIVEN that a device event has already been received
|
// GIVEN that a device event has already been received
|
||||||
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
// WHEN media event is received
|
// WHEN media event is received
|
||||||
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
|
||||||
// THEN the listener receives a combined event
|
// THEN the listener receives a combined event
|
||||||
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
||||||
verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
|
verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
|
||||||
assertThat(captor.getValue().getDevice()).isNotNull();
|
assertThat(captor.getValue().getDevice()).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void emitEventAfterMediaFirst() {
|
public void emitEventAfterMediaFirst() {
|
||||||
// GIVEN that media event has already been received
|
// GIVEN that media event has already been received
|
||||||
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
|
||||||
// WHEN device event is received
|
// WHEN device event is received
|
||||||
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
// THEN the listener receives a combined event
|
// THEN the listener receives a combined event
|
||||||
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
||||||
verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
|
verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
|
||||||
assertThat(captor.getValue().getDevice()).isNotNull();
|
assertThat(captor.getValue().getDevice()).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void mediaDataRemovedAfterMediaEvent() {
|
public void mediaDataRemovedAfterMediaEvent() {
|
||||||
mDataListener.onMediaDataLoaded(KEY, mMediaData);
|
mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
|
||||||
mDataListener.onMediaDataRemoved(KEY);
|
mDataListener.onMediaDataRemoved(KEY);
|
||||||
verify(mListener).onMediaDataRemoved(eq(KEY));
|
verify(mListener).onMediaDataRemoved(eq(KEY));
|
||||||
}
|
}
|
||||||
@@ -145,6 +145,18 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
|
|||||||
verify(mListener).onMediaDataRemoved(eq(KEY));
|
verify(mListener).onMediaDataRemoved(eq(KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mediaDataKeyUpdated() {
|
||||||
|
// GIVEN that device and media events have already been received
|
||||||
|
mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
|
||||||
|
mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
|
||||||
|
// WHEN the key is changed
|
||||||
|
mDataListener.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
|
||||||
|
// THEN the listener gets a load event with the correct keys
|
||||||
|
ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
|
||||||
|
verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture());
|
||||||
|
}
|
||||||
|
|
||||||
private MediaDataManager.Listener captureDataListener() {
|
private MediaDataManager.Listener captureDataListener() {
|
||||||
ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass(
|
ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass(
|
||||||
MediaDataManager.Listener.class);
|
MediaDataManager.Listener.class);
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import android.media.MediaRouter2Manager
|
|||||||
import android.media.RoutingSessionInfo
|
import android.media.RoutingSessionInfo
|
||||||
import android.media.session.MediaSession
|
import android.media.session.MediaSession
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.os.Process
|
|
||||||
import android.service.notification.StatusBarNotification
|
|
||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import android.testing.TestableLooper
|
import android.testing.TestableLooper
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
@@ -67,6 +65,7 @@ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
|||||||
public class MediaDeviceManagerTest : SysuiTestCase() {
|
public class MediaDeviceManagerTest : SysuiTestCase() {
|
||||||
|
|
||||||
private lateinit var manager: MediaDeviceManager
|
private lateinit var manager: MediaDeviceManager
|
||||||
|
@Mock private lateinit var mediaDataManager: MediaDataManager
|
||||||
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
|
@Mock private lateinit var lmmFactory: LocalMediaManagerFactory
|
||||||
@Mock private lateinit var lmm: LocalMediaManager
|
@Mock private lateinit var lmm: LocalMediaManager
|
||||||
@Mock private lateinit var mr2: MediaRouter2Manager
|
@Mock private lateinit var mr2: MediaRouter2Manager
|
||||||
@@ -80,13 +79,14 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
private lateinit var metadataBuilder: MediaMetadata.Builder
|
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||||
private lateinit var playbackBuilder: PlaybackState.Builder
|
private lateinit var playbackBuilder: PlaybackState.Builder
|
||||||
private lateinit var notifBuilder: Notification.Builder
|
private lateinit var notifBuilder: Notification.Builder
|
||||||
private lateinit var sbn: StatusBarNotification
|
private lateinit var mediaData: MediaData
|
||||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
fakeExecutor = FakeExecutor(FakeSystemClock())
|
fakeExecutor = FakeExecutor(FakeSystemClock())
|
||||||
manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor)
|
manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor,
|
||||||
|
mediaDataManager)
|
||||||
manager.addListener(listener)
|
manager.addListener(listener)
|
||||||
|
|
||||||
// Configure mocks.
|
// Configure mocks.
|
||||||
@@ -117,8 +117,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
setSmallIcon(android.R.drawable.ic_media_pause)
|
setSmallIcon(android.R.drawable.ic_media_pause)
|
||||||
setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
|
setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
|
||||||
}
|
}
|
||||||
sbn = StatusBarNotification(PACKAGE, PACKAGE, 0, "TAG", Process.myUid(), 0, 0,
|
mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||||
notifBuilder.build(), Process.myUserHandle(), 0)
|
emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -128,33 +128,33 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun removeUnknown() {
|
fun removeUnknown() {
|
||||||
manager.onNotificationRemoved("unknown")
|
manager.onMediaDataRemoved("unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun addNotification() {
|
fun addNotification() {
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
verify(lmmFactory).create(PACKAGE)
|
verify(lmmFactory).create(PACKAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun featureDisabled() {
|
fun featureDisabled() {
|
||||||
whenever(featureFlag.enabled).thenReturn(false)
|
whenever(featureFlag.enabled).thenReturn(false)
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
verify(lmmFactory, never()).create(PACKAGE)
|
verify(lmmFactory, never()).create(PACKAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun addAndRemoveNotification() {
|
fun addAndRemoveNotification() {
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
manager.onNotificationRemoved(KEY)
|
manager.onMediaDataRemoved(KEY)
|
||||||
verify(lmm).unregisterCallback(any())
|
verify(lmm).unregisterCallback(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deviceEventOnAddNotification() {
|
fun deviceEventOnAddNotification() {
|
||||||
// WHEN a notification is added
|
// WHEN a notification is added
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
val deviceCallback = captureCallback()
|
val deviceCallback = captureCallback()
|
||||||
// THEN the update is dispatched to the listener
|
// THEN the update is dispatched to the listener
|
||||||
val data = captureDeviceData(KEY)
|
val data = captureDeviceData(KEY)
|
||||||
@@ -165,7 +165,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deviceListUpdate() {
|
fun deviceListUpdate() {
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
val deviceCallback = captureCallback()
|
val deviceCallback = captureCallback()
|
||||||
// WHEN the device list changes
|
// WHEN the device list changes
|
||||||
deviceCallback.onDeviceListUpdate(mutableListOf(device))
|
deviceCallback.onDeviceListUpdate(mutableListOf(device))
|
||||||
@@ -179,7 +179,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun selectedDeviceStateChanged() {
|
fun selectedDeviceStateChanged() {
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
val deviceCallback = captureCallback()
|
val deviceCallback = captureCallback()
|
||||||
// WHEN the selected device changes state
|
// WHEN the selected device changes state
|
||||||
deviceCallback.onSelectedDeviceStateChanged(device, 1)
|
deviceCallback.onSelectedDeviceStateChanged(device, 1)
|
||||||
@@ -193,9 +193,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun listenerReceivesKeyRemoved() {
|
fun listenerReceivesKeyRemoved() {
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
// WHEN the notification is removed
|
// WHEN the notification is removed
|
||||||
manager.onNotificationRemoved(KEY)
|
manager.onMediaDataRemoved(KEY)
|
||||||
// THEN the listener receives key removed event
|
// THEN the listener receives key removed event
|
||||||
verify(listener).onKeyRemoved(eq(KEY))
|
verify(listener).onKeyRemoved(eq(KEY))
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
// GIVEN that MR2Manager returns null for routing session
|
// GIVEN that MR2Manager returns null for routing session
|
||||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
||||||
// WHEN a notification is added
|
// WHEN a notification is added
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
// THEN the device is disabled
|
// THEN the device is disabled
|
||||||
val data = captureDeviceData(KEY)
|
val data = captureDeviceData(KEY)
|
||||||
assertThat(data.enabled).isFalse()
|
assertThat(data.enabled).isFalse()
|
||||||
@@ -216,7 +216,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
|
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
|
||||||
// GIVEN a notif is added
|
// GIVEN a notif is added
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
reset(listener)
|
reset(listener)
|
||||||
// AND MR2Manager returns null for routing session
|
// AND MR2Manager returns null for routing session
|
||||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
||||||
@@ -234,7 +234,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
|
fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
|
||||||
// GIVEN a notif is added
|
// GIVEN a notif is added
|
||||||
manager.onNotificationAdded(KEY, sbn)
|
manager.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
reset(listener)
|
reset(listener)
|
||||||
// GIVEN that MR2Manager returns null for routing session
|
// GIVEN that MR2Manager returns null for routing session
|
||||||
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
package com.android.systemui.media
|
package com.android.systemui.media
|
||||||
|
|
||||||
|
import android.media.MediaMetadata
|
||||||
import android.media.session.MediaController
|
import android.media.session.MediaController
|
||||||
|
import android.media.session.MediaSession
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
@@ -41,6 +43,10 @@ import org.mockito.Mockito.verify
|
|||||||
import org.mockito.junit.MockitoJUnit
|
import org.mockito.junit.MockitoJUnit
|
||||||
|
|
||||||
private const val KEY = "KEY"
|
private const val KEY = "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 fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||||
private fun <T> anyObject(): T {
|
private fun <T> anyObject(): T {
|
||||||
@@ -54,12 +60,15 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
|
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
|
||||||
@Mock private lateinit var mediaController: MediaController
|
@Mock private lateinit var mediaController: MediaController
|
||||||
@Mock private lateinit var executor: DelayableExecutor
|
@Mock private lateinit var executor: DelayableExecutor
|
||||||
@Mock private lateinit var mediaData: MediaData
|
|
||||||
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||||
@Mock private lateinit var cancellationRunnable: Runnable
|
@Mock private lateinit var cancellationRunnable: Runnable
|
||||||
@Captor private lateinit var timeoutCaptor: ArgumentCaptor<Runnable>
|
@Captor private lateinit var timeoutCaptor: ArgumentCaptor<Runnable>
|
||||||
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
||||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||||
|
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||||
|
private lateinit var playbackBuilder: PlaybackState.Builder
|
||||||
|
private lateinit var session: MediaSession
|
||||||
|
private lateinit var mediaData: MediaData
|
||||||
private lateinit var mediaTimeoutListener: MediaTimeoutListener
|
private lateinit var mediaTimeoutListener: MediaTimeoutListener
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -68,22 +77,39 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
`when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable)
|
`when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable)
|
||||||
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor)
|
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor)
|
||||||
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
||||||
|
|
||||||
|
// Create a media session and notification for testing.
|
||||||
|
metadataBuilder = MediaMetadata.Builder().apply {
|
||||||
|
putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
|
||||||
|
putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
|
||||||
|
}
|
||||||
|
playbackBuilder = PlaybackState.Builder().apply {
|
||||||
|
setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
|
||||||
|
setActions(PlaybackState.ACTION_PLAY)
|
||||||
|
}
|
||||||
|
session = MediaSession(context, SESSION_KEY).apply {
|
||||||
|
setMetadata(metadataBuilder.build())
|
||||||
|
setPlaybackState(playbackBuilder.build())
|
||||||
|
}
|
||||||
|
session.setActive(true)
|
||||||
|
mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
|
||||||
|
emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnMediaDataLoaded_registersPlaybackListener() {
|
fun testOnMediaDataLoaded_registersPlaybackListener() {
|
||||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
|
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
|
||||||
|
|
||||||
// Ignores is same key
|
// Ignores is same key
|
||||||
clearInvocations(mediaController)
|
clearInvocations(mediaController)
|
||||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
|
||||||
verify(mediaController, never()).registerCallback(anyObject())
|
verify(mediaController, never()).registerCallback(anyObject())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnMediaDataRemoved_unregistersPlaybackListener() {
|
fun testOnMediaDataRemoved_unregistersPlaybackListener() {
|
||||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
mediaTimeoutListener.onMediaDataRemoved(KEY)
|
mediaTimeoutListener.onMediaDataRemoved(KEY)
|
||||||
verify(mediaController).unregisterCallback(anyObject())
|
verify(mediaController).unregisterCallback(anyObject())
|
||||||
|
|
||||||
@@ -124,7 +150,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIsTimedOut() {
|
fun testIsTimedOut() {
|
||||||
mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
|
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
|
assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user