/* * 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. * *

This intent will have 2 extras: *

* *

{@link #EXTRA_STATE} can be any of * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. * *

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. * *

this intent will have the two extras: *

*/ public static final String ACTION_TRACK_EVENT = "android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT"; /** * Intent used to broadcast the change of folder list. * *

This intent will have the one extra: *

*/ 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 getConnectedDevices() { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); List devices = new ArrayList(); if (mConnectedDevice != null) { devices.add(mConnectedDevice); } return devices; } /** * This function only supports STATE_CONNECTED */ public synchronized List getDevicesMatchingConnectionStates(int[] states) { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); List devices = new ArrayList(); 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 getConnectedDevices() { AvrcpControllerService service = getService(); if (service == null) { return new ArrayList(0); } return service.getConnectedDevices(); } @Override public List getDevicesMatchingConnectionStates(int[] states) { AvrcpControllerService service = getService(); if (service == null) { return new ArrayList(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 attrList = new ArrayList<>(); for (int attr : attributes) { attrList.add(attr); } List 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 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 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); }