blob: 6c6aef483c3873f66b79ff5ed602564571e9de96 [file] [log] [blame]
/*
* Copyright (C) 2016 Google Inc.
*
* 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.googlecode.android_scripting.facade.bluetooth;
import android.app.Service;
import android.content.Intent;
import android.content.ComponentName;
import android.content.Context;
import android.media.MediaMetadata;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcParameter;
import com.googlecode.android_scripting.Log;
import java.util.List;
/**
* SL4A Facade for dealing with Bluetooth Media related test cases
* The APIs here can be used on a AVRCP CT and TG, depending on what is tested.
*/
public class BluetoothMediaFacade extends RpcReceiver {
private static final String TAG = "BluetoothMediaFacade";
private static final boolean VDBG = false;
private final Service mService;
private final Context mContext;
private Handler mHandler;
private MediaSessionManager mSessionManager;
private MediaController mMediaController = null;
private MediaController.Callback mMediaCtrlCallback = null;
private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
private MediaBrowser mBrowser = null;
private static EventFacade mEventFacade;
// Events posted
private static final String EVENT_PLAY_RECEIVED = "playReceived";
private static final String EVENT_PAUSE_RECEIVED = "pauseReceived";
private static final String EVENT_SKIP_PREV_RECEIVED = "skipPrevReceived";
private static final String EVENT_SKIP_NEXT_RECEIVED = "skipNextReceived";
// Commands received
private static final String CMD_MEDIA_PLAY = "play";
private static final String CMD_MEDIA_PAUSE = "pause";
private static final String CMD_MEDIA_SKIP_NEXT = "skipNext";
private static final String CMD_MEDIA_SKIP_PREV = "skipPrev";
private static final String bluetoothPkgName = "com.android.bluetooth";
private static final String browserServiceName =
"com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService";
public BluetoothMediaFacade(FacadeManager manager) {
super(manager);
mService = manager.getService();
mEventFacade = manager.getReceiver(EventFacade.class);
mHandler = new Handler(Looper.getMainLooper());
mContext = mService.getApplicationContext();
mSessionManager =
(MediaSessionManager) mContext.getSystemService(mContext.MEDIA_SESSION_SERVICE);
mSessionListener = new SessionChangeListener();
// Listen on Active MediaSession changes, so we can get the active session's MediaController
if (mSessionManager != null) {
ComponentName compName =
new ComponentName(mContext.getPackageName(), this.getClass().getName());
mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, null,
mHandler);
if (VDBG) {
List<MediaController> mcl = mSessionManager.getActiveSessions(null);
Log.d(TAG + " Num Sessions " + mcl.size());
for (int i = 0; i < mcl.size(); i++) {
Log.d(TAG + "Active session : " + i + ((MediaController) (mcl.get(
i))).getPackageName() + ((MediaController) (mcl.get(i))).getTag());
}
}
}
mMediaCtrlCallback = new MediaControllerCallback();
}
/**
* Class method called from {@link BluetoothAvrcpMediaBrowserService} to post an Event through
* EventFacade back to the RPC client.
*
* @param playbackState PlaybackState change that is posted as an Event to the client.
*/
public static void dispatchPlaybackStateChanged(int playbackState) {
switch (playbackState) {
case PlaybackState.STATE_PLAYING:
mEventFacade.postEvent(EVENT_PLAY_RECEIVED, new Bundle());
break;
case PlaybackState.STATE_PAUSED:
mEventFacade.postEvent(EVENT_PAUSE_RECEIVED, new Bundle());
break;
case PlaybackState.STATE_SKIPPING_TO_NEXT:
mEventFacade.postEvent(EVENT_SKIP_NEXT_RECEIVED, new Bundle());
break;
case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
mEventFacade.postEvent(EVENT_SKIP_PREV_RECEIVED, new Bundle());
break;
default:
break;
}
}
/**
* To register to MediaSession for callbacks on updates
*/
private class MediaControllerCallback extends MediaController.Callback {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
Log.d(TAG + " onPlaybackStateChanged: " + state.getState());
}
@Override
public void onMetadataChanged(MediaMetadata metadata) {
Log.d(TAG + " onMetadataChanged ");
}
}
/**
* To get the MediaController for the currently active MediaSession
*/
private class SessionChangeListener
implements MediaSessionManager.OnActiveSessionsChangedListener {
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
if (VDBG) {
Log.d(TAG + " onActiveSessionsChanged : " + controllers.size());
for (int i = 0; i < controllers.size(); i++) {
Log.d(TAG + "Active session : " + i + ((MediaController) (controllers.get(
i))).getPackageName() + ((MediaController) (controllers.get(
i))).getTag());
}
}
// Whenever the list of ActiveSessions change, iterate through the list of active
// session and look for the one that belongs to the BluetoothAvrcpMediaBrowserService.
// If found, update our MediaController, we don't care for the other Active Media
// Sessions.
if (controllers.size() > 0) {
for (int i = 0; i < controllers.size(); i++) {
MediaController controller = (MediaController) controllers.get(i);
if (controller.getTag().contains(BluetoothAvrcpMediaBrowserService.getTag())) {
setCurrentMediaController(controller);
return;
}
}
setCurrentMediaController(null);
} else {
setCurrentMediaController(null);
}
}
}
/**
* Callback on <code>MediaBrowser.connect()</code>
*/
MediaBrowser.ConnectionCallback mBrowserConnectionCallback =
new MediaBrowser.ConnectionCallback() {
private static final String classTag = TAG + " BrowserConnectionCallback";
@Override
public void onConnected() {
Log.d(classTag + " onConnected: session token " + mBrowser.getSessionToken());
MediaController mediaController = new MediaController(mContext,
mBrowser.getSessionToken());
// Update the MediaController
setCurrentMediaController(mediaController);
}
@Override
public void onConnectionFailed() {
Log.d(classTag + " onConnectionFailed");
}
};
/**
* Update the MediaController.
* On the AVRCP CT side (Carkitt for ex), this MediaController
* would be the one associated with the
* com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService.
* On the AVRCP TG side (Phone for ex), this MediaController would
* be the one associated with the
* com.googlecode.android_scripting.facade.bluetooth.BluetoothAvrcpMediaBrowserService
*/
private void setCurrentMediaController(MediaController controller) {
Handler mainHandler = new Handler(mContext.getMainLooper());
if (mMediaController == null && controller != null) {
Log.d(TAG + " Setting MediaController " + controller.getTag());
mMediaController = controller;
mMediaController.registerCallback(mMediaCtrlCallback);
} else if (mMediaController != null && controller != null) {
// We have a diff media controller
if (controller.getSessionToken().equals(mMediaController.getSessionToken())
== false) {
Log.d(TAG + " Changing MediaController " + controller.getTag());
mMediaController.unregisterCallback(mMediaCtrlCallback);
mMediaController = controller;
mMediaController.registerCallback(mMediaCtrlCallback, mainHandler);
}
} else if (mMediaController != null && controller == null) {
// The new media controller is null probably because it doesn't support Transport
// Controls
Log.d(TAG + " Clearing MediaController " + mMediaController.getTag());
mMediaController.unregisterCallback(mMediaCtrlCallback);
mMediaController = controller;
}
}
// Sends the passthrough command through the currently active MediaController.
// If there isn't one, look for the currently active sessions and just pick the first one,
// just a fallback
@Rpc(description = "Simulate a passthrough command")
public void bluetoothMediaPassthrough(
@RpcParameter(name = "passthruCmd", description = "play/pause/skipFwd/skipBack")
String passthruCmd) {
Log.d(TAG + "Passthrough Cmd " + passthruCmd);
if (mMediaController == null) {
Log.i(TAG + " Media Controller not ready - Grabbing existing one");
ComponentName name =
new ComponentName(mContext.getPackageName(),
mSessionListener.getClass().getName());
List<MediaController> listMC = mSessionManager.getActiveSessions(null);
if (listMC.size() > 0) {
if (VDBG) {
Log.d(TAG + " Num Sessions " + listMC.size());
for (int i = 0; i < listMC.size(); i++) {
Log.d(TAG + "Active session : " + i + ((MediaController) (listMC.get(
i))).getPackageName() + ((MediaController) (listMC.get(
i))).getTag());
}
}
mMediaController = (MediaController) listMC.get(0);
} else {
Log.d(TAG + " No Active Media Session to grab");
return;
}
}
switch (passthruCmd) {
case CMD_MEDIA_PLAY:
mMediaController.getTransportControls().play();
break;
case CMD_MEDIA_PAUSE:
mMediaController.getTransportControls().pause();
break;
case CMD_MEDIA_SKIP_NEXT:
mMediaController.getTransportControls().skipToNext();
break;
case CMD_MEDIA_SKIP_PREV:
mMediaController.getTransportControls().skipToPrevious();
break;
default:
Log.d(TAG + " Unsupported Passthrough Cmd");
break;
}
}
// This is usually called from the AVRCP CT device (Carkitt) to connect to the
// existing com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserservice
@Rpc(description = "Connect a MediaBrowser to the A2dpMediaBrowserservice in the Carkitt")
public void bluetoothMediaConnectToA2dpMediaBrowserService() {
ComponentName compName;
// Create a MediaBrowser to connect to the A2dpMBS
if (mBrowser == null) {
compName = new ComponentName(bluetoothPkgName, browserServiceName);
// Note - MediaBrowser connect needs to be done on the Main Thread's handler,
// otherwise we never get the ServiceConnected callback.
Runnable createAndConnectMediaBrowser = new Runnable() {
@Override
public void run() {
mBrowser = new MediaBrowser(mContext, compName, mBrowserConnectionCallback,
null);
if (mBrowser != null) {
Log.d(TAG + " Connecting to MBS");
mBrowser.connect();
} else {
Log.d(TAG + " Failed to create a MediaBrowser");
}
}
};
Handler mainHandler = new Handler(mContext.getMainLooper());
mainHandler.post(createAndConnectMediaBrowser);
} //mBrowser
}
// This is usually called from the AVRCP TG device (Phone)
@Rpc(description = "Start the BluetoothAvrcpMediaBrowserService.")
public void bluetoothMediaAvrcpMediaBrowserServiceStart() {
Log.d(TAG + "Staring BluetoothAvrcpMediaBrowserService");
// Start the Avrcp Media Browser service. Starting it sets it to active.
Intent startIntent = new Intent(mContext, BluetoothAvrcpMediaBrowserService.class);
mContext.startService(startIntent);
}
@Rpc(description = "Stop the BluetoothAvrcpMediaBrowserService.")
public void bluetoothMediaAvrcpMediaBrowserServiceStop() {
Log.d(TAG + "Stopping BluetoothAvrcpMediaBrowserService");
// Stop the Avrcp Media Browser service.
Intent stopIntent = new Intent(mContext, BluetoothAvrcpMediaBrowserService.class);
mContext.stopService(stopIntent);
}
@Override
public void shutdown() {
setCurrentMediaController(null);
}
}