blob: 7ff0a25a44e4a2d02e128557d5b72fdf5f62bb12 [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.bluetooth.avrcpcontroller;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAvrcpPlayerSettings;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothAvrcpController;
import android.content.Context;
import android.media.AudioManager;
import android.media.browse.MediaBrowser;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.ProfileService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Provides Bluetooth AVRCP Controller profile, as a service in the Bluetooth application.
*/
public class AvrcpControllerService extends ProfileService {
static final String TAG = "AvrcpControllerService";
static final boolean DBG = true;
static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
/*
* Play State Values from JNI
*/
private static final byte JNI_PLAY_STATUS_STOPPED = 0x00;
private static final byte JNI_PLAY_STATUS_PLAYING = 0x01;
private static final byte JNI_PLAY_STATUS_PAUSED = 0x02;
private static final byte JNI_PLAY_STATUS_FWD_SEEK = 0x03;
private static final byte JNI_PLAY_STATUS_REV_SEEK = 0x04;
private static final byte JNI_PLAY_STATUS_ERROR = -1;
/*
* Browsing Media Item Attribute IDs
* This should be kept in sync with BTRC_MEDIA_ATTR_ID_* in bt_rc.h
*/
private static final int JNI_MEDIA_ATTR_ID_INVALID = -1;
private static final int JNI_MEDIA_ATTR_ID_TITLE = 0x00000001;
private static final int JNI_MEDIA_ATTR_ID_ARTIST = 0x00000002;
private static final int JNI_MEDIA_ATTR_ID_ALBUM = 0x00000003;
private static final int JNI_MEDIA_ATTR_ID_TRACK_NUM = 0x00000004;
private static final int JNI_MEDIA_ATTR_ID_NUM_TRACKS = 0x00000005;
private static final int JNI_MEDIA_ATTR_ID_GENRE = 0x00000006;
private static final int JNI_MEDIA_ATTR_ID_PLAYING_TIME = 0x00000007;
/*
* Browsing folder types
* This should be kept in sync with BTRC_FOLDER_TYPE_* in bt_rc.h
*/
private static final int JNI_FOLDER_TYPE_TITLES = 0x01;
private static final int JNI_FOLDER_TYPE_ALBUMS = 0x02;
private static final int JNI_FOLDER_TYPE_ARTISTS = 0x03;
private static final int JNI_FOLDER_TYPE_GENRES = 0x04;
private static final int JNI_FOLDER_TYPE_PLAYLISTS = 0x05;
private static final int JNI_FOLDER_TYPE_YEARS = 0x06;
/*
* AVRCP Error types as defined in spec. Also they should be in sync with btrc_status_t.
* NOTE: Not all may be defined.
*/
private static final int JNI_AVRC_STS_NO_ERROR = 0x04;
private static final int JNI_AVRC_INV_RANGE = 0x0b;
/**
* Intent used to broadcast the change in browse connection state of the AVRCP Controller
* profile.
*
* <p>This intent will have 2 extras:
* <ul>
* <li> {@link BluetoothProfile#EXTRA_STATE} - The current state of the profile. </li>
* <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
* </ul>
*
* <p>{@link #EXTRA_STATE} can be any of
* {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
* {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
* receive.
*/
public static final String ACTION_BROWSE_CONNECTION_STATE_CHANGED =
"android.bluetooth.avrcp-controller.profile.action.BROWSE_CONNECTION_STATE_CHANGED";
/**
* intent used to broadcast the change in metadata state of playing track on the avrcp
* ag.
*
* <p>this intent will have the two extras:
* <ul>
* <li> {@link #extra_metadata} - {@link mediametadata} containing the current metadata.</li>
* <li> {@link #extra_playback} - {@link playbackstate} containing the current playback
* state. </li>
* </ul>
*/
public static final String ACTION_TRACK_EVENT =
"android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT";
/**
* Intent used to broadcast the change of folder list.
*
* <p>This intent will have the one extra:
* <ul>
* <li> {@link #EXTRA_FOLDER_LIST} - array of {@link MediaBrowser#MediaItem}
* containing the folder listing of currently selected folder.
* </ul>
*/
public static final String ACTION_FOLDER_LIST =
"android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST";
public static final String EXTRA_FOLDER_LIST =
"android.bluetooth.avrcp-controller.profile.extra.FOLDER_LIST";
public static final String EXTRA_FOLDER_ID = "com.android.bluetooth.avrcp.EXTRA_FOLDER_ID";
public static final String EXTRA_FOLDER_BT_ID =
"com.android.bluetooth.avrcp-controller.EXTRA_FOLDER_BT_ID";
public static final String EXTRA_METADATA =
"android.bluetooth.avrcp-controller.profile.extra.METADATA";
public static final String EXTRA_PLAYBACK =
"android.bluetooth.avrcp-controller.profile.extra.PLAYBACK";
public static final String MEDIA_ITEM_UID_KEY = "media-item-uid-key";
/*
* KeyCoded for Pass Through Commands
*/
public static final int PASS_THRU_CMD_ID_PLAY = 0x44;
public static final int PASS_THRU_CMD_ID_PAUSE = 0x46;
public static final int PASS_THRU_CMD_ID_VOL_UP = 0x41;
public static final int PASS_THRU_CMD_ID_VOL_DOWN = 0x42;
public static final int PASS_THRU_CMD_ID_STOP = 0x45;
public static final int PASS_THRU_CMD_ID_FF = 0x49;
public static final int PASS_THRU_CMD_ID_REWIND = 0x48;
public static final int PASS_THRU_CMD_ID_FORWARD = 0x4B;
public static final int PASS_THRU_CMD_ID_BACKWARD = 0x4C;
/* Key State Variables */
public static final int KEY_STATE_PRESSED = 0;
public static final int KEY_STATE_RELEASED = 1;
/* Group Navigation Key Codes */
public static final int PASS_THRU_CMD_ID_NEXT_GRP = 0x00;
public static final int PASS_THRU_CMD_ID_PREV_GRP = 0x01;
/* Folder navigation directions
* This is borrowed from AVRCP 1.6 spec and must be kept with same values
*/
public static final int FOLDER_NAVIGATION_DIRECTION_UP = 0x00;
public static final int FOLDER_NAVIGATION_DIRECTION_DOWN = 0x01;
/* Folder/Media Item scopes.
* Keep in sync with AVRCP 1.6 sec. 6.10.1
*/
public static final int BROWSE_SCOPE_PLAYER_LIST = 0x00;
public static final int BROWSE_SCOPE_VFS = 0x01;
public static final int BROWSE_SCOPE_SEARCH = 0x02;
public static final int BROWSE_SCOPE_NOW_PLAYING = 0x03;
private AvrcpControllerStateMachine mAvrcpCtSm;
private static AvrcpControllerService sAvrcpControllerService;
// UID size is 8 bytes (AVRCP 1.6 spec)
private static final byte[] EMPTY_UID = {0, 0, 0, 0, 0, 0, 0, 0};
// We only support one device.
private BluetoothDevice mConnectedDevice = null;
// If browse is supported (only valid if mConnectedDevice != null).
private boolean mBrowseConnected = false;
// Caches the current browse folder. If this is null then root is the currently browsed folder
// (which also has no UID).
private String mCurrentBrowseFolderUID = null;
static {
classInitNative();
}
public AvrcpControllerService() {
initNative();
}
protected String getName() {
return TAG;
}
protected IProfileServiceBinder initBinder() {
return new BluetoothAvrcpControllerBinder(this);
}
protected boolean start() {
HandlerThread thread = new HandlerThread("BluetoothAvrcpHandler");
thread.start();
mAvrcpCtSm = new AvrcpControllerStateMachine(this);
mAvrcpCtSm.start();
setAvrcpControllerService(this);
return true;
}
protected boolean stop() {
if (mAvrcpCtSm != null) {
mAvrcpCtSm.doQuit();
}
return true;
}
//API Methods
public static synchronized AvrcpControllerService getAvrcpControllerService() {
if (sAvrcpControllerService != null && sAvrcpControllerService.isAvailable()) {
if (DBG) {
Log.d(TAG, "getAvrcpControllerService(): returning "
+ sAvrcpControllerService);
}
return sAvrcpControllerService;
}
if (DBG) {
if (sAvrcpControllerService == null) {
Log.d(TAG, "getAvrcpControllerService(): service is NULL");
} else if (!(sAvrcpControllerService.isAvailable())) {
Log.d(TAG, "getAvrcpControllerService(): service is not available");
}
}
return null;
}
private static synchronized void setAvrcpControllerService(AvrcpControllerService instance) {
if (instance != null && instance.isAvailable()) {
if (DBG) {
Log.d(TAG, "setAvrcpControllerService(): set to: " + sAvrcpControllerService);
}
sAvrcpControllerService = instance;
} else {
if (DBG) {
if (instance == null) {
Log.d(TAG, "setAvrcpControllerService(): service not available");
} else if (!instance.isAvailable()) {
Log.d(TAG, "setAvrcpControllerService(): service is cleaning up");
}
}
}
}
private static synchronized void clearAvrcpControllerService() {
sAvrcpControllerService = null;
}
public synchronized List<BluetoothDevice> getConnectedDevices() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
if (mConnectedDevice != null) {
devices.add(mConnectedDevice);
}
return devices;
}
/**
* This function only supports STATE_CONNECTED
*/
public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
for (int i = 0; i < states.length; i++) {
if (states[i] == BluetoothProfile.STATE_CONNECTED && mConnectedDevice != null) {
devices.add(mConnectedDevice);
}
}
return devices;
}
public synchronized int getConnectionState(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return (mConnectedDevice != null ? BluetoothProfile.STATE_CONNECTED :
BluetoothProfile.STATE_DISCONNECTED);
}
public synchronized void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
Log.v(TAG, "sendGroupNavigationCmd keyCode: " + keyCode + " keyState: " + keyState);
if (device == null) {
Log.e(TAG, "sendGroupNavigationCmd device is null");
}
if (!(device.equals(mConnectedDevice))) {
Log.e(TAG, " Device does not match " + device + " connected " + mConnectedDevice);
return;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_SEND_GROUP_NAVIGATION_CMD, keyCode, keyState, device);
mAvrcpCtSm.sendMessage(msg);
}
public synchronized void sendPassThroughCmd(BluetoothDevice device, int keyCode, int keyState) {
Log.v(TAG, "sendPassThroughCmd keyCode: " + keyCode + " keyState: " + keyState);
if (device == null) {
Log.e(TAG, "sendPassThroughCmd Device is null");
return;
}
if (!device.equals(mConnectedDevice)) {
Log.w(TAG, " Device does not match device " + device + " conn " + mConnectedDevice);
return;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mAvrcpCtSm
.obtainMessage(AvrcpControllerStateMachine.MESSAGE_SEND_PASS_THROUGH_CMD,
keyCode, keyState, device);
mAvrcpCtSm.sendMessage(msg);
}
public void startAvrcpUpdates() {
mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_START_METADATA_BROADCASTS).sendToTarget();
}
public void stopAvrcpUpdates() {
mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_STOP_METADATA_BROADCASTS).sendToTarget();
}
public synchronized MediaMetadata getMetaData(BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "getMetaData");
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
if (device == null) {
Log.e(TAG, "getMetadata device is null");
return null;
}
if (!device.equals(mConnectedDevice)) {
return null;
}
return mAvrcpCtSm.getCurrentMetaData();
}
public PlaybackState getPlaybackState(BluetoothDevice device) {
// Get the cached state by default.
return getPlaybackState(device, true);
}
// cached can be used to force a getPlaybackState command. Useful for PTS testing.
public synchronized PlaybackState getPlaybackState(BluetoothDevice device, boolean cached) {
if (DBG) {
Log.d(TAG, "getPlayBackState device = " + device);
}
if (device == null) {
Log.e(TAG, "getPlaybackState device is null");
return null;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "Device " + device + " does not match connected deivce " + mConnectedDevice);
return null;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return mAvrcpCtSm.getCurrentPlayBackState(cached);
}
public synchronized BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "getPlayerApplicationSetting ");
}
if (device == null) {
Log.e(TAG, "getPlayerSettings device is null");
return null;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "device " + device + " does not match connected device " + mConnectedDevice);
return null;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
/* Do nothing */
return null;
}
public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
if (DBG) {
Log.d(TAG, "getPlayerApplicationSetting");
}
/* Do nothing */
return false;
}
/**
* Fetches the list of children for the parentID node.
*
* This function manages the overall tree for browsing structure.
*
* Arguments:
* device - Device to browse content for.
* parentMediaId - ID of the parent that we need to browse content for. Since most
* of the players are database unware, fetching a root invalidates all the children.
* start - number of item to start scanning from
* items - number of items to fetch
*/
public synchronized boolean getChildren(
BluetoothDevice device, String parentMediaId, int start, int items) {
if (DBG) {
Log.d(TAG, "getChildren device = " + device + " parent " + parentMediaId);
}
if (device == null) {
Log.e(TAG, "getChildren device is null");
return false;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "getChildren device " + device + " does not match " +
mConnectedDevice);
return false;
}
if (!mBrowseConnected) {
Log.e(TAG, "getChildren browse not yet connected");
return false;
}
if (!mAvrcpCtSm.isConnected()) {
return false;
}
mAvrcpCtSm.getChildren(parentMediaId, start, items);
return true;
}
public synchronized boolean getNowPlayingList(
BluetoothDevice device, String id, int start, int items) {
if (DBG) {
Log.d(TAG, "getNowPlayingList device = " + device + " start = " + start +
"items = " + items);
}
if (device == null) {
Log.e(TAG, "getNowPlayingList device is null");
return false;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "getNowPlayingList device " + device + " does not match " +
mConnectedDevice);
return false;
}
if (!mBrowseConnected) {
Log.e(TAG, "getNowPlayingList browse not yet connected");
return false;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST, start, items, id);
mAvrcpCtSm.sendMessage(msg);
return true;
}
public synchronized boolean getFolderList(
BluetoothDevice device, String id, int start, int items) {
if (DBG) {
Log.d(TAG, "getFolderListing device = " + device + " start = " + start +
"items = " + items);
}
if (device == null) {
Log.e(TAG, "getFolderListing device is null");
return false;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "getFolderListing device " + device + " does not match " + mConnectedDevice);
return false;
}
if (!mBrowseConnected) {
Log.e(TAG, "getFolderListing browse not yet connected");
return false;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, start, items, id);
mAvrcpCtSm.sendMessage(msg);
return true;
}
public synchronized boolean getPlayerList(BluetoothDevice device, int start, int items) {
if (DBG) {
Log.d(TAG, "getPlayerList device = " + device + " start = " + start +
"items = " + items);
}
if (device == null) {
Log.e(TAG, "getPlayerList device is null");
return false;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "getPlayerList device " + device + " does not match " + mConnectedDevice);
return false;
}
if (!mBrowseConnected) {
Log.e(TAG, "getPlayerList browse not yet connected");
return false;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start, items);
mAvrcpCtSm.sendMessage(msg);
return true;
}
public synchronized boolean changeFolderPath(
BluetoothDevice device, int direction, String uid, String fid) {
if (DBG) {
Log.d(TAG, "changeFolderPath device = " + device + " direction " +
direction + " uid " + uid);
}
if (device == null) {
Log.e(TAG, "changeFolderPath device is null");
return false;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "changeFolderPath device " + device + " does not match " +
mConnectedDevice);
return false;
}
if (!mBrowseConnected) {
Log.e(TAG, "changeFolderPath browse not yet connected");
return false;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Bundle b = new Bundle();
b.putString(EXTRA_FOLDER_ID, fid);
b.putString(EXTRA_FOLDER_BT_ID, uid);
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_CHANGE_FOLDER_PATH, direction, 0, b);
mAvrcpCtSm.sendMessage(msg);
return true;
}
public synchronized boolean setBrowsedPlayer(BluetoothDevice device, int id, String fid) {
if (DBG) {
Log.d(TAG, "setBrowsedPlayer device = " + device + " id" + id + " fid " + fid);
}
if (device == null) {
Log.e(TAG, "setBrowsedPlayer device is null");
return false;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "changeFolderPath device " + device + " does not match " +
mConnectedDevice);
return false;
}
if (!mBrowseConnected) {
Log.e(TAG, "setBrowsedPlayer browse not yet connected");
return false;
}
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER, id, 0, fid);
mAvrcpCtSm.sendMessage(msg);
return true;
}
public synchronized void fetchAttrAndPlayItem(BluetoothDevice device, String uid) {
if (DBG) {
Log.d(TAG, "fetchAttrAndPlayItem device = " + device + " uid " + uid);
}
if (device == null) {
Log.e(TAG, "fetchAttrAndPlayItem device is null");
return;
}
if (!device.equals(mConnectedDevice)) {
Log.e(TAG, "fetchAttrAndPlayItem device " + device + " does not match " +
mConnectedDevice);
return;
}
if (!mBrowseConnected) {
Log.e(TAG, "fetchAttrAndPlayItem browse not yet connected");
return;
}
mAvrcpCtSm.fetchAttrAndPlayItem(uid);
}
//Binder object: Must be static class or memory leak may occur
private static class BluetoothAvrcpControllerBinder extends IBluetoothAvrcpController.Stub
implements IProfileServiceBinder {
private AvrcpControllerService mService;
private AvrcpControllerService getService() {
if (!Utils.checkCaller()) {
Log.w(TAG, "AVRCP call not allowed for non-active user");
return null;
}
if (mService != null && mService.isAvailable()) {
return mService;
}
return null;
}
BluetoothAvrcpControllerBinder(AvrcpControllerService svc) {
mService = svc;
}
public boolean cleanup() {
mService = null;
return true;
}
@Override
public List<BluetoothDevice> getConnectedDevices() {
AvrcpControllerService service = getService();
if (service == null) {
return new ArrayList<BluetoothDevice>(0);
}
return service.getConnectedDevices();
}
@Override
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
AvrcpControllerService service = getService();
if (service == null) {
return new ArrayList<BluetoothDevice>(0);
}
return service.getDevicesMatchingConnectionStates(states);
}
@Override
public int getConnectionState(BluetoothDevice device) {
AvrcpControllerService service = getService();
if (service == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
if (device == null) {
throw new IllegalStateException("Device cannot be null!");
}
return service.getConnectionState(device);
}
@Override
public void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
Log.v(TAG, "Binder Call: sendGroupNavigationCmd");
AvrcpControllerService service = getService();
if (service == null) {
return;
}
if (device == null) {
throw new IllegalStateException("Device cannot be null!");
}
service.sendGroupNavigationCmd(device, keyCode, keyState);
}
@Override
public BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
Log.v(TAG, "Binder Call: getPlayerApplicationSetting ");
AvrcpControllerService service = getService();
if (service == null) {
return null;
}
if (device == null) {
throw new IllegalStateException("Device cannot be null!");
}
return service.getPlayerSettings(device);
}
@Override
public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
Log.v(TAG, "Binder Call: setPlayerApplicationSetting ");
AvrcpControllerService service = getService();
if (service == null) {
return false;
}
return service.setPlayerApplicationSetting(plAppSetting);
}
}
// Called by JNI when a passthrough key was received.
private void handlePassthroughRsp(int id, int keyState, byte[] address) {
Log.d(TAG, "passthrough response received as: key: " + id + " state: " + keyState +
"address:" + address);
}
private void handleGroupNavigationRsp(int id, int keyState) {
Log.d(TAG, "group navigation response received as: key: " + id + " state: " + keyState);
}
// Called by JNI when a device has connected or disconnected.
private synchronized void onConnectionStateChanged(
boolean rc_connected, boolean br_connected, byte[] address) {
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
Log.d(TAG, "onConnectionStateChanged " + rc_connected + " " + br_connected +
device + " conn device " + mConnectedDevice);
if (device == null) {
Log.e(TAG, "onConnectionStateChanged Device is null");
return;
}
// Adjust the AVRCP connection state.
int oldState = (device.equals(mConnectedDevice) ? BluetoothProfile.STATE_CONNECTED :
BluetoothProfile.STATE_DISCONNECTED);
int newState = (rc_connected ? BluetoothProfile.STATE_CONNECTED :
BluetoothProfile.STATE_DISCONNECTED);
if (rc_connected && oldState == BluetoothProfile.STATE_DISCONNECTED) {
/* AVRCPControllerService supports single connection */
if (mConnectedDevice != null) {
Log.d(TAG, "A Connection already exists, returning");
return;
}
mConnectedDevice = device;
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_CONNECTION_CHANGE, newState,
oldState, device);
mAvrcpCtSm.sendMessage(msg);
} else if (!rc_connected && oldState == BluetoothProfile.STATE_CONNECTED) {
mConnectedDevice = null;
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_CONNECTION_CHANGE, newState,
oldState, device);
mAvrcpCtSm.sendMessage(msg);
}
// Adjust the browse connection state. If RC is connected we should have already sent the
// connection status out.
if (rc_connected && br_connected) {
mBrowseConnected = true;
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE);
msg.arg1 = 1;
msg.obj = device;
mAvrcpCtSm.sendMessage(msg);
}
}
// Called by JNI to notify Avrcp of features supported by the Remote device.
private void getRcFeatures(byte[] address, int features) {
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_RC_FEATURES, features, 0, device);
mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI
private void setPlayerAppSettingRsp(byte[] address, byte accepted) {
/* Do Nothing. */
}
// Called by JNI when remote wants to receive absolute volume notifications.
private synchronized void handleRegisterNotificationAbsVol(byte[] address, byte label) {
Log.d(TAG, "handleRegisterNotificationAbsVol ");
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "handleRegisterNotificationAbsVol device not found " + address);
return;
}
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION, (int) label, 0);
mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI when remote wants to set absolute volume.
private synchronized void handleSetAbsVolume(byte[] address, byte absVol, byte label) {
Log.d(TAG, "handleSetAbsVolume ");
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "handleSetAbsVolume device not found " + address);
return;
}
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ABS_VOL_CMD, absVol, label);
mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI when a track changes and local AvrcpController is registered for updates.
private synchronized void onTrackChanged(byte[] address, byte numAttributes, int[] attributes,
String[] attribVals) {
if (DBG) {
Log.d(TAG, "onTrackChanged");
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "onTrackChanged device not found " + address);
return;
}
List<Integer> attrList = new ArrayList<>();
for (int attr : attributes) {
attrList.add(attr);
}
List<String> attrValList = Arrays.asList(attribVals);
TrackInfo trackInfo = new TrackInfo(attrList, attrValList);
if (DBG) {
Log.d(TAG, "onTrackChanged " + trackInfo);
}
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_TRACK_CHANGED, trackInfo);
mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI periodically based upon timer to update play position
private synchronized void onPlayPositionChanged(byte[] address, int songLen, int currSongPosition) {
if (DBG) {
Log.d(TAG, "onPlayPositionChanged pos " + currSongPosition);
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "onPlayPositionChanged not found device not found " + address);
return;
}
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_PLAY_POS_CHANGED, songLen, currSongPosition);
mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI on changes of play status
private synchronized void onPlayStatusChanged(byte[] address, byte playStatus) {
if (DBG) {
Log.d(TAG, "onPlayStatusChanged " + playStatus);
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "onPlayStatusChanged not found device not found " + address);
return;
}
int playbackState = PlaybackState.STATE_NONE;
switch (playStatus) {
case JNI_PLAY_STATUS_STOPPED:
playbackState = PlaybackState.STATE_STOPPED;
break;
case JNI_PLAY_STATUS_PLAYING:
playbackState = PlaybackState.STATE_PLAYING;
break;
case JNI_PLAY_STATUS_PAUSED:
playbackState = PlaybackState.STATE_PAUSED;
break;
case JNI_PLAY_STATUS_FWD_SEEK:
playbackState = PlaybackState.STATE_FAST_FORWARDING;
break;
case JNI_PLAY_STATUS_REV_SEEK:
playbackState = PlaybackState.STATE_FAST_FORWARDING;
break;
default:
playbackState = PlaybackState.STATE_NONE;
}
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_PLAY_STATUS_CHANGED, playbackState);
mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI to report remote Player's capabilities
private synchronized void handlePlayerAppSetting(byte[] address, byte[] playerAttribRsp, int rspLen) {
if (DBG) {
Log.d(TAG, "handlePlayerAppSetting rspLen = " + rspLen);
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "handlePlayerAppSetting not found device not found " + address);
return;
}
PlayerApplicationSettings supportedSettings = PlayerApplicationSettings.
makeSupportedSettings(playerAttribRsp);
/* Do nothing */
}
private synchronized void onPlayerAppSettingChanged(byte[] address, byte[] playerAttribRsp, int rspLen) {
if (DBG) {
Log.d(TAG, "onPlayerAppSettingChanged ");
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
if (device != null && !device.equals(mConnectedDevice)) {
Log.e(TAG, "onPlayerAppSettingChanged not found device not found " + address);
return;
}
PlayerApplicationSettings desiredSettings = PlayerApplicationSettings.
makeSettings(playerAttribRsp);
/* Do nothing */
}
// Browsing related JNI callbacks.
void handleGetFolderItemsRsp(int status, MediaItem[] items) {
if (DBG) {
Log.d(TAG, "handleGetFolderItemsRsp called with status " + status +
" items " + items.length + " items.");
}
if (status == JNI_AVRC_INV_RANGE) {
Log.w(TAG, "Sending out of range message.");
// Send a special message since this could be used by state machine
// to take as a signal that fetch is finished.
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE);
mAvrcpCtSm.sendMessage(msg);
return;
}
for (MediaItem item : items) {
if (DBG) {
Log.d(TAG, "media item: " + item + " uid: " + item.getDescription().getMediaId());
}
}
ArrayList<MediaItem> itemsList = new ArrayList<>();
for (MediaItem item : items) {
itemsList.add(item);
}
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_GET_FOLDER_ITEMS, itemsList);
mAvrcpCtSm.sendMessage(msg);
}
void handleGetPlayerItemsRsp(AvrcpPlayer[] items) {
if (DBG) {
Log.d(TAG, "handleGetFolderItemsRsp called with " + items.length + " items.");
}
for (AvrcpPlayer item : items) {
if (DBG) {
Log.d(TAG, "bt player item: " + item);
}
}
List<AvrcpPlayer> itemsList = new ArrayList<>();
for (AvrcpPlayer p : items) {
itemsList.add(p);
}
Message msg = mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.
MESSAGE_PROCESS_GET_PLAYER_ITEMS, itemsList);
mAvrcpCtSm.sendMessage(msg);
}
// JNI Helper functions to convert native objects to java.
MediaItem createFromNativeMediaItem(
byte[] uid, int type, String name, int[] attrIds, String[] attrVals) {
if (DBG) {
Log.d(TAG, "createFromNativeMediaItem uid: " + uid + " type " + type + " name " +
name + " attrids " + attrIds + " attrVals " + attrVals);
}
MediaDescription.Builder mdb = new MediaDescription.Builder();
Bundle mdExtra = new Bundle();
mdExtra.putString(MEDIA_ITEM_UID_KEY, byteUIDToHexString(uid));
mdb.setExtras(mdExtra);
// Generate a random UUID. We do this since database unaware TGs can send multiple
// items with same MEDIA_ITEM_UID_KEY.
mdb.setMediaId(UUID.randomUUID().toString());
// Concise readable name.
mdb.setTitle(name);
// We skip the attributes since we can query them using UID for the item above
// Also MediaDescription does not give an easy way to provide this unless we pass
// it as an MediaMetadata which is put inside the extras.
return new MediaItem(mdb.build(), MediaItem.FLAG_PLAYABLE);
}
MediaItem createFromNativeFolderItem(
byte[] uid, int type, String name, int playable) {
if (DBG) {
Log.d(TAG, "createFromNativeFolderItem uid: " + uid + " type " + type +
" name " + name + " playable " + playable);
}
MediaDescription.Builder mdb = new MediaDescription.Builder();
// Covert the byte to a hex string. The coversion can be done back here to a
// byte array when needed.
Bundle mdExtra = new Bundle();
mdExtra.putString(MEDIA_ITEM_UID_KEY, byteUIDToHexString(uid));
mdb.setExtras(mdExtra);
// Generate a random UUID. We do this since database unaware TGs can send multiple
// items with same MEDIA_ITEM_UID_KEY.
mdb.setMediaId(UUID.randomUUID().toString());
// Concise readable name.
mdb.setTitle(name);
return new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE);
}
AvrcpPlayer createFromNativePlayerItem(
int id, String name, byte[] transportFlags, int playStatus, int playerType) {
if (DBG) {
Log.d(TAG, "createFromNativePlayerItem name: " + name + " transportFlags " +
transportFlags + " play status " + playStatus + " player type " +
playerType);
}
AvrcpPlayer player = new AvrcpPlayer(id, name, 0, playStatus, playerType);
return player;
}
private void handleChangeFolderRsp(int count) {
if (DBG) {
Log.d(TAG, "handleChangeFolderRsp count: " + count);
}
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_FOLDER_PATH, count);
mAvrcpCtSm.sendMessage(msg);
}
private void handleSetBrowsedPlayerRsp(int items, int depth) {
if (DBG) {
Log.d(TAG, "handleSetBrowsedPlayerRsp depth: " + depth);
}
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_BROWSED_PLAYER, items, depth);
mAvrcpCtSm.sendMessage(msg);
}
private void handleSetAddressedPlayerRsp(int status) {
if (DBG) {
Log.d(TAG, "handleSetAddressedPlayerRsp status: " + status);
}
Message msg = mAvrcpCtSm.obtainMessage(
AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
mAvrcpCtSm.sendMessage(msg);
}
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
mAvrcpCtSm.dump(sb);
}
public static String byteUIDToHexString(byte[] uid) {
StringBuilder sb = new StringBuilder();
for (byte b : uid) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
public static byte[] hexStringToByteUID(String uidStr) {
if (uidStr == null) {
Log.e(TAG, "Null hex string.");
return EMPTY_UID;
} else if (uidStr.length() % 2 == 1) {
// Odd length strings should not be possible.
Log.e(TAG, "Odd length hex string " + uidStr);
return EMPTY_UID;
}
int len = uidStr.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(uidStr.charAt(i), 16) << 4)
+ Character.digit(uidStr.charAt(i + 1), 16));
}
return data;
}
private native static void classInitNative();
private native void initNative();
private native void cleanupNative();
native static boolean sendPassThroughCommandNative(byte[] address, int keyCode, int keyState);
native static boolean sendGroupNavigationCommandNative(byte[] address, int keyCode,
int keyState);
native static void setPlayerApplicationSettingValuesNative(byte[] address, byte numAttrib,
byte[] atttibIds, byte[] attribVal);
/* This api is used to send response to SET_ABS_VOL_CMD */
native static void sendAbsVolRspNative(byte[] address, int absVol, int label);
/* This api is used to inform remote for any volume level changes */
native static void sendRegisterAbsVolRspNative(byte[] address, byte rspType, int absVol,
int label);
/* API used to fetch the playback state */
native static void getPlaybackStateNative(byte[] address);
/* API used to fetch the current now playing list */
native static void getNowPlayingListNative(byte[] address, byte start, byte end);
/* API used to fetch the current folder's listing */
native static void getFolderListNative(byte[] address, byte start, byte end);
/* API used to fetch the listing of players */
native static void getPlayerListNative(byte[] address, byte start, byte end);
/* API used to change the folder */
native static void changeFolderPathNative(byte[] address, byte direction, byte[] uid);
native static void playItemNative(
byte[] address, byte scope, byte[] uid, int uidCounter);
native static void setBrowsedPlayerNative(byte[] address, int playerId);
native static void setAddressedPlayerNative(byte[] address, int playerId);
}