blob: f6cd971ec4f31f5bc5bea70e71cb85102152cf00 [file] [log] [blame]
/*
* Copyright (C) 2019 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.settingslib.volume;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.media.IRemoteVolumeController;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession.QueueItem;
import android.media.session.MediaSession.Token;
import android.media.session.MediaSessionManager;
import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Convenience client for all media session updates. Provides a callback interface for events
* related to remote media sessions.
*/
public class MediaSessions {
private static final String TAG = Util.logTag(MediaSessions.class);
private static final boolean USE_SERVICE_LABEL = false;
private final Context mContext;
private final H mHandler;
private final MediaSessionManager mMgr;
private final Map<Token, MediaControllerRecord> mRecords = new HashMap<>();
private final Callbacks mCallbacks;
private boolean mInit;
public MediaSessions(Context context, Looper looper, Callbacks callbacks) {
mContext = context;
mHandler = new H(looper);
mMgr = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
mCallbacks = callbacks;
}
/**
* Dump to {@code writer}
*/
public void dump(PrintWriter writer) {
writer.println(getClass().getSimpleName() + " state:");
writer.print(" mInit: ");
writer.println(mInit);
writer.print(" mRecords.size: ");
writer.println(mRecords.size());
int i = 0;
for (MediaControllerRecord r : mRecords.values()) {
dump(++i, writer, r.controller);
}
}
/**
* init MediaSessions
*/
public void init() {
if (D.BUG) Log.d(TAG, "init");
// will throw if no permission
mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler);
mInit = true;
postUpdateSessions();
mMgr.registerRemoteVolumeController(mRvc);
}
protected void postUpdateSessions() {
if (!mInit) return;
mHandler.sendEmptyMessage(H.UPDATE_SESSIONS);
}
/**
* Destroy MediaSessions
*/
public void destroy() {
if (D.BUG) Log.d(TAG, "destroy");
mInit = false;
mMgr.removeOnActiveSessionsChangedListener(mSessionsListener);
mMgr.unregisterRemoteVolumeController(mRvc);
}
/**
* Set volume {@code level} to remote media {@code token}
*/
public void setVolume(Token token, int level) {
final MediaControllerRecord r = mRecords.get(token);
if (r == null) {
Log.w(TAG, "setVolume: No record found for token " + token);
return;
}
if (D.BUG) Log.d(TAG, "Setting level to " + level);
r.controller.setVolumeTo(level, 0);
}
private void onRemoteVolumeChangedH(Token sessionToken, int flags) {
final MediaController controller = new MediaController(mContext, sessionToken);
if (D.BUG) {
Log.d(TAG, "remoteVolumeChangedH " + controller.getPackageName() + " "
+ Util.audioManagerFlagsToString(flags));
}
final Token token = controller.getSessionToken();
mCallbacks.onRemoteVolumeChanged(token, flags);
}
private void onUpdateRemoteControllerH(Token sessionToken) {
final MediaController controller =
sessionToken != null ? new MediaController(mContext, sessionToken) : null;
final String pkg = controller != null ? controller.getPackageName() : null;
if (D.BUG) Log.d(TAG, "updateRemoteControllerH " + pkg);
// this may be our only indication that a remote session is changed, refresh
postUpdateSessions();
}
protected void onActiveSessionsUpdatedH(List<MediaController> controllers) {
if (D.BUG) Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size());
final Set<Token> toRemove = new HashSet<Token>(mRecords.keySet());
for (MediaController controller : controllers) {
final Token token = controller.getSessionToken();
final PlaybackInfo pi = controller.getPlaybackInfo();
toRemove.remove(token);
if (!mRecords.containsKey(token)) {
final MediaControllerRecord r = new MediaControllerRecord(controller);
r.name = getControllerName(controller);
mRecords.put(token, r);
controller.registerCallback(r, mHandler);
}
final MediaControllerRecord r = mRecords.get(token);
final boolean remote = isRemote(pi);
if (remote) {
updateRemoteH(token, r.name, pi);
r.sentRemote = true;
}
}
for (Token t : toRemove) {
final MediaControllerRecord r = mRecords.get(t);
r.controller.unregisterCallback(r);
mRecords.remove(t);
if (D.BUG) Log.d(TAG, "Removing " + r.name + " sentRemote=" + r.sentRemote);
if (r.sentRemote) {
mCallbacks.onRemoteRemoved(t);
r.sentRemote = false;
}
}
}
private static boolean isRemote(PlaybackInfo pi) {
return pi != null && pi.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
}
protected String getControllerName(MediaController controller) {
final PackageManager pm = mContext.getPackageManager();
final String pkg = controller.getPackageName();
try {
if (USE_SERVICE_LABEL) {
final List<ResolveInfo> ris = pm.queryIntentServices(
new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0);
if (ris != null) {
for (ResolveInfo ri : ris) {
if (ri.serviceInfo == null) continue;
if (pkg.equals(ri.serviceInfo.packageName)) {
final String serviceLabel =
Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim();
if (serviceLabel.length() > 0) {
return serviceLabel;
}
}
}
}
}
final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim();
if (appLabel.length() > 0) {
return appLabel;
}
} catch (NameNotFoundException e) {
}
return pkg;
}
private void updateRemoteH(Token token, String name, PlaybackInfo pi) {
if (mCallbacks != null) {
mCallbacks.onRemoteUpdate(token, name, pi);
}
}
private static void dump(int n, PrintWriter writer, MediaController c) {
writer.println(" Controller " + n + ": " + c.getPackageName());
final Bundle extras = c.getExtras();
final long flags = c.getFlags();
final MediaMetadata mm = c.getMetadata();
final PlaybackInfo pi = c.getPlaybackInfo();
final PlaybackState playbackState = c.getPlaybackState();
final List<QueueItem> queue = c.getQueue();
final CharSequence queueTitle = c.getQueueTitle();
final int ratingType = c.getRatingType();
final PendingIntent sessionActivity = c.getSessionActivity();
writer.println(" PlaybackState: " + Util.playbackStateToString(playbackState));
writer.println(" PlaybackInfo: " + Util.playbackInfoToString(pi));
if (mm != null) {
writer.println(" MediaMetadata.desc=" + mm.getDescription());
}
writer.println(" RatingType: " + ratingType);
writer.println(" Flags: " + flags);
if (extras != null) {
writer.println(" Extras:");
for (String key : extras.keySet()) {
writer.println(" " + key + "=" + extras.get(key));
}
}
if (queueTitle != null) {
writer.println(" QueueTitle: " + queueTitle);
}
if (queue != null && !queue.isEmpty()) {
writer.println(" Queue:");
for (QueueItem qi : queue) {
writer.println(" " + qi);
}
}
if (pi != null) {
writer.println(" sessionActivity: " + sessionActivity);
}
}
private final class MediaControllerRecord extends MediaController.Callback {
public final MediaController controller;
public boolean sentRemote;
public String name;
private MediaControllerRecord(MediaController controller) {
this.controller = controller;
}
private String cb(String method) {
return method + " " + controller.getPackageName() + " ";
}
@Override
public void onAudioInfoChanged(PlaybackInfo info) {
if (D.BUG) {
Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info)
+ " sentRemote=" + sentRemote);
}
final boolean remote = isRemote(info);
if (!remote && sentRemote) {
mCallbacks.onRemoteRemoved(controller.getSessionToken());
sentRemote = false;
} else if (remote) {
updateRemoteH(controller.getSessionToken(), name, info);
sentRemote = true;
}
}
@Override
public void onExtrasChanged(Bundle extras) {
if (D.BUG) Log.d(TAG, cb("onExtrasChanged") + extras);
}
@Override
public void onMetadataChanged(MediaMetadata metadata) {
if (D.BUG) Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata));
}
@Override
public void onPlaybackStateChanged(PlaybackState state) {
if (D.BUG) Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state));
}
@Override
public void onQueueChanged(List<QueueItem> queue) {
if (D.BUG) Log.d(TAG, cb("onQueueChanged") + queue);
}
@Override
public void onQueueTitleChanged(CharSequence title) {
if (D.BUG) Log.d(TAG, cb("onQueueTitleChanged") + title);
}
@Override
public void onSessionDestroyed() {
if (D.BUG) Log.d(TAG, cb("onSessionDestroyed"));
}
@Override
public void onSessionEvent(String event, Bundle extras) {
if (D.BUG) Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras);
}
}
private final OnActiveSessionsChangedListener mSessionsListener =
new OnActiveSessionsChangedListener() {
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
onActiveSessionsUpdatedH(controllers);
}
};
private final IRemoteVolumeController mRvc = new IRemoteVolumeController.Stub() {
@Override
public void remoteVolumeChanged(Token sessionToken, int flags)
throws RemoteException {
mHandler.obtainMessage(H.REMOTE_VOLUME_CHANGED, flags, 0,
sessionToken).sendToTarget();
}
@Override
public void updateRemoteController(final Token sessionToken)
throws RemoteException {
mHandler.obtainMessage(H.UPDATE_REMOTE_CONTROLLER, sessionToken).sendToTarget();
}
};
private final class H extends Handler {
private static final int UPDATE_SESSIONS = 1;
private static final int REMOTE_VOLUME_CHANGED = 2;
private static final int UPDATE_REMOTE_CONTROLLER = 3;
private H(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_SESSIONS:
onActiveSessionsUpdatedH(mMgr.getActiveSessions(null));
break;
case REMOTE_VOLUME_CHANGED:
onRemoteVolumeChangedH((Token) msg.obj, msg.arg1);
break;
case UPDATE_REMOTE_CONTROLLER:
onUpdateRemoteControllerH((Token) msg.obj);
break;
}
}
}
/**
* Callback for remote media sessions
*/
public interface Callbacks {
/**
* Invoked when remote media session is updated
*/
void onRemoteUpdate(Token token, String name, PlaybackInfo pi);
/**
* Invoked when remote media session is removed
*/
void onRemoteRemoved(Token t);
/**
* Invoked when remote volume is changed
*/
void onRemoteVolumeChanged(Token token, int flags);
}
}