blob: e57485249c6431d6ab95654661cc3a61fc060881 [file] [log] [blame]
/*
* Copyright (c) 2016, 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.car.stream.media;
import android.content.Context;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.KeyEvent;
import com.android.car.apps.common.util.Assert;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* A class to listen for changes in sessions from {@link MediaSessionManager}. It also notifies
* listeners of changes in the playback state or metadata.
*/
public class MediaStateManager {
private static final String TAG = "MediaStateManager";
private static final String TELECOM_PACKAGE = "com.android.server.telecom";
private final Context mContext;
private MediaAppInfo mConnectedAppInfo;
private MediaController mController;
private Handler mHandler;
private final Set<Listener> mListeners;
public interface Listener {
void onMediaSessionConnected(PlaybackState playbackState, MediaMetadata metaData,
MediaAppInfo appInfo);
void onPlaybackStateChanged(@Nullable PlaybackState state);
void onMetadataChanged(@Nullable MediaMetadata metadata);
void onSessionDestroyed();
}
public MediaStateManager(@NonNull Context context) {
mContext = context;
mHandler = new Handler(Looper.getMainLooper());
mListeners = new LinkedHashSet<>();
}
public void start() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Starting MediaStateManager");
}
MediaSessionManager sessionManager
= (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
try {
sessionManager.addOnActiveSessionsChangedListener(mSessionChangedListener, null);
List<MediaController> controllers = sessionManager.getActiveSessions(null);
updateMediaController(controllers);
} catch (SecurityException e) {
// User hasn't granted the permission so we should just go away silently.
}
}
@MainThread
public void destroy() {
Assert.isMainThread();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "destroy()");
}
stop();
mListeners.clear();
mHandler = null;
}
@MainThread
public void stop() {
Assert.isMainThread();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "stop()");
}
if (mController != null) {
mController.unregisterCallback(mMediaControllerCallback);
mController = null;
}
// Calling this with null will clear queue of callbacks and message. This needs to be done
// here because prior to the above lines to disconnect and unregister the
// controller a posted runnable to do work maybe have happened and thus we need to clear it
// out to prevent race conditions.
mHandler.removeCallbacksAndMessages(null);
}
public void dispatchMediaButton(KeyEvent keyEvent) {
if (mController != null) {
MediaController.TransportControls transportControls
= mController.getTransportControls();
int eventId = keyEvent.getKeyCode();
switch (eventId) {
case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD:
transportControls.skipToPrevious();
break;
case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD:
transportControls.skipToNext();
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
transportControls.play();
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
transportControls.pause();
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
transportControls.stop();
break;
default:
mController.dispatchMediaButtonEvent(keyEvent);
}
}
}
public void addListener(@NonNull Listener listener) {
mListeners.add(listener);
}
public void removeListener(@NonNull Listener listener) {
mListeners.remove(listener);
}
private void updateMediaController(List<MediaController> controllers) {
if (controllers.size() > 0) {
// If the telecom package is trying to onStart a media session, ignore it
// so that the existing media item continues to appear in the stream.
if (TELECOM_PACKAGE.equals(controllers.get(0).getPackageName())) {
return;
}
if (mController != null) {
mController.unregisterCallback(mMediaControllerCallback);
}
// Currently the first controller is the active one playing music.
// If this is no longer the case, consider checking notification listener
// for a MediaStyle notification to get currently playing media app.
mController = controllers.get(0);
mController.registerCallback(mMediaControllerCallback);
mConnectedAppInfo = new MediaAppInfo(mContext, mController.getPackageName());
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updating media controller");
}
for (Listener listener : mListeners) {
listener.onMediaSessionConnected(mController.getPlaybackState(),
mController.getMetadata(), mConnectedAppInfo);
}
} else {
Log.w(TAG, "Updating controllers with an empty list!");
}
}
public static boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
private final MediaSessionManager.OnActiveSessionsChangedListener
mSessionChangedListener = new MediaSessionManager.OnActiveSessionsChangedListener() {
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
updateMediaController(controllers);
}
};
private final MediaController.Callback mMediaControllerCallback =
new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(@NonNull final PlaybackState state) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onPlaybackStateChanged(" + state + ")");
}
for (Listener listener : mListeners) {
listener.onPlaybackStateChanged(state);
}
}
});
}
@Override
public void onMetadataChanged(@Nullable final MediaMetadata metadata) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onMetadataChanged(" + metadata + ")");
}
for (Listener listener : mListeners) {
listener.onMetadataChanged(metadata);
}
}
});
}
@Override
public void onSessionDestroyed() {
mHandler.post(new Runnable() {
@Override
public void run() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onSessionDestroyed()");
}
mConnectedAppInfo = null;
if (mController != null) {
mController.unregisterCallback(mMediaControllerCallback);
mController = null;
}
for (Listener listener : mListeners) {
listener.onSessionDestroyed();
}
}
});
}
};
}