Merge "BT: Converging AVRCP files for multi-connection FR" into bt.lnx.5.0
diff --git a/packages_apps_bluetooth_ext/AddressedMediaPlayer_ext.java b/packages_apps_bluetooth_ext/AddressedMediaPlayer_ext.java
new file mode 100644
index 0000000..5e61b75
--- /dev/null
+++ b/packages_apps_bluetooth_ext/AddressedMediaPlayer_ext.java
@@ -0,0 +1,639 @@
+/*
+ * 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.avrcp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.MediaSession.QueueItem;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.ProfileService;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/*************************************************************************************************
+ * Provides functionality required for Addressed Media Player, like Now Playing List related
+ * browsing commands, control commands to the current addressed player(playItem, play, pause, etc)
+ * Acts as an Interface to communicate with media controller APIs for NowPlayingItems.
+ ************************************************************************************************/
+
+public class AddressedMediaPlayer_ext {
+ private static final String TAG = "AddressedMediaPlayer_ext";
+ private static final Boolean DEBUG = true;
+
+ private static final long SINGLE_QID = 1;
+ private static final String UNKNOWN_TITLE = "(unknown)";
+
+ static private final String GPM_BUNDLE_METADATA_KEY =
+ "com.google.android.music.mediasession.music_metadata";
+
+ private AvrcpMediaRspInterface mMediaInterface;
+ private Avrcp_ext mAvrcp = null;
+ @NonNull private List<MediaSession.QueueItem> mNowPlayingList;
+
+ private final List<MediaSession.QueueItem> mEmptyNowPlayingList;
+
+ private long mLastTrackIdSent;
+
+ public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
+ mEmptyNowPlayingList = new ArrayList<MediaSession.QueueItem>();
+ mNowPlayingList = mEmptyNowPlayingList;
+ mMediaInterface = mediaInterface;
+ mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
+ }
+
+ public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface, Avrcp_ext mAvrcp_ext) {
+ this(mediaInterface);
+ mAvrcp = mAvrcp_ext;
+ }
+
+ void cleanup() {
+ if (DEBUG) {
+ Log.v(TAG, "cleanup");
+ }
+ mNowPlayingList = mEmptyNowPlayingList;
+ mMediaInterface = null;
+ mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
+ }
+
+ /* get now playing list from addressed player */
+ void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
+ @Nullable MediaController mediaController) {
+ if (mediaController == null) {
+ // No players (if a player exists, we would have selected it)
+ Log.e(TAG, "mediaController = null, sending no available players response");
+ mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
+ return;
+ }
+ List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
+ getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING,
+ reqObj.mStartItem, reqObj.mEndItem, mediaController);
+ }
+
+ /* get item attributes for item in now playing list */
+ void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
+ @Nullable MediaController mediaController) {
+ int status = AvrcpConstants.RSP_NO_ERROR;
+ long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
+ List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
+
+ // NOTE: this is out-of-spec (AVRCP 1.6.1 sec 6.10.4.3, p90) but we answer it anyway
+ // because some CTs ask for it.
+ if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
+ mediaId = getActiveQueueItemId(mediaController);
+ if (DEBUG) {
+ Log.d(TAG, "getItemAttr: Remote requests for now playing contents, sending UID: "
+ + mediaId);
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid));
+ }
+ for (MediaSession.QueueItem item : items) {
+ if (item.getQueueId() == mediaId) {
+ getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController);
+ return;
+ }
+ }
+
+ // Couldn't find it, so the id is invalid
+ mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null);
+ }
+
+ /* Refresh and get the queue of now playing.
+ */
+ @NonNull
+ List<MediaSession.QueueItem> updateNowPlayingList(@Nullable MediaController mediaController) {
+ if (mediaController == null) {
+ return mEmptyNowPlayingList;
+ }
+ List<MediaSession.QueueItem> items = mediaController.getQueue();
+ if (items == null) {
+ Log.i(TAG, "null queue from " + mediaController.getPackageName()
+ + ", constructing single-item list");
+
+ // Because we are database-unaware, we can just number the item here whatever we want
+ // because they have to re-poll it every time.
+ MediaMetadata metadata = mediaController.getMetadata();
+ if (metadata == null) {
+ Log.w(TAG, "Controller has no metadata!? Making an empty one");
+ metadata = (new MediaMetadata.Builder()).build();
+ }
+
+ MediaDescription.Builder bob = new MediaDescription.Builder();
+ MediaDescription desc = metadata.getDescription();
+
+ // set the simple ones that MediaMetadata builds for us
+ bob.setMediaId(desc.getMediaId());
+ bob.setTitle(desc.getTitle());
+ bob.setSubtitle(desc.getSubtitle());
+ bob.setDescription(desc.getDescription());
+ // fill the ones that we use later
+ bob.setExtras(fillBundle(metadata, desc.getExtras()));
+
+ // build queue item with the new metadata
+ MediaSession.QueueItem current = new QueueItem(bob.build(), SINGLE_QID);
+
+ items = new ArrayList<MediaSession.QueueItem>();
+ items.add(current);
+ }
+
+ if (!items.equals(mNowPlayingList)) {
+ sendNowPlayingListChanged();
+ }
+ mNowPlayingList = items;
+
+ return mNowPlayingList;
+ }
+
+ private void sendNowPlayingListChanged() {
+ if (mMediaInterface == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "sendNowPlayingListChanged()");
+ }
+ mMediaInterface.nowPlayingChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
+ }
+
+ private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) {
+ if (metadata == null) {
+ Log.i(TAG, "fillBundle: metadata is null");
+ return currentExtras;
+ }
+
+ Bundle bundle = currentExtras;
+ if (bundle == null) {
+ bundle = new Bundle();
+ }
+
+ String[] stringKeys = {
+ MediaMetadata.METADATA_KEY_TITLE,
+ MediaMetadata.METADATA_KEY_ARTIST,
+ MediaMetadata.METADATA_KEY_ALBUM,
+ MediaMetadata.METADATA_KEY_GENRE
+ };
+ for (String key : stringKeys) {
+ String current = bundle.getString(key);
+ if (current == null) {
+ bundle.putString(key, metadata.getString(key));
+ }
+ }
+
+ String[] longKeys = {
+ MediaMetadata.METADATA_KEY_TRACK_NUMBER,
+ MediaMetadata.METADATA_KEY_NUM_TRACKS,
+ MediaMetadata.METADATA_KEY_DURATION
+ };
+ for (String key : longKeys) {
+ if (!bundle.containsKey(key)) {
+ bundle.putLong(key, metadata.getLong(key));
+ }
+ }
+ return bundle;
+ }
+
+ /* Instructs media player to play particular media item */
+ void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
+ long qid = ByteBuffer.wrap(uid).getLong();
+ List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
+
+ if (mediaController == null) {
+ Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
+ mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
+ return;
+ }
+
+ MediaController.TransportControls mediaControllerCntrl =
+ mediaController.getTransportControls();
+
+ if (items == null) {
+ Log.w(TAG, "nowPlayingItems is null");
+ mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
+ return;
+ }
+
+ for (MediaSession.QueueItem item : items) {
+ if (qid == item.getQueueId()) {
+ if (DEBUG) {
+ Log.d(TAG, "Skipping to ID " + qid);
+ }
+ mediaControllerCntrl.skipToQueueItem(qid);
+ mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR);
+ return;
+ }
+ }
+
+ Log.w(TAG, "Invalid now playing Queue ID " + qid);
+ mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
+ }
+
+ void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
+ List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
+ if (DEBUG) {
+ Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items.");
+ }
+ mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
+ }
+
+ void sendTrackChangeWithId(int type, @Nullable MediaController mediaController) {
+ Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController);
+ long qid = getActiveQueueItemId(mediaController);
+ byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
+ Log.d(TAG, "qid: " + qid );
+ if ((type == AvrcpConstants.NOTIFICATION_TYPE_INTERIM) &&
+ (qid != MediaSession.QueueItem.UNKNOWN_ID) &&
+ (qid != mLastTrackIdSent)) {
+ byte[] lastTrack =
+ ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(mLastTrackIdSent).array();
+ mMediaInterface.trackChangedRsp(type, lastTrack);
+ type = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
+ }
+ // The nowPlayingList changed: the new list has the full data for the current item
+ Log.d(TAG, "last_sent_qid: " + mLastTrackIdSent);
+ mMediaInterface.trackChangedRsp(type, track);
+ mLastTrackIdSent = qid;
+ }
+
+ void sendTrackChangeWithId(int type, @Nullable MediaController mediaController, byte[] bdaddr) {
+ Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController);
+ if(mAvrcp == null) {
+ sendTrackChangeWithId(type, mediaController);
+ return;
+ }
+ long qid = getActiveQueueItemId(mediaController);
+ byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
+ Log.d(TAG, "qid: " + qid );
+ if ((type == AvrcpConstants.NOTIFICATION_TYPE_INTERIM) &&
+ (qid != MediaSession.QueueItem.UNKNOWN_ID) &&
+ (qid != mLastTrackIdSent)) {
+ byte[] lastTrack =
+ ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(mLastTrackIdSent).array();
+ mAvrcp.trackChangedAddressedRsp(type, lastTrack, bdaddr);
+ type = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
+ }
+ // The nowPlayingList changed: the new list has the full data for the current item
+ Log.d(TAG, "last_sent_qid: " + mLastTrackIdSent);
+ mAvrcp.trackChangedAddressedRsp(type, track, bdaddr);
+ mLastTrackIdSent = qid;
+ }
+
+ /*
+ * helper method to check if startItem and endItem index is with range of
+ * MediaItem list. (Resultset containing all items in current path)
+ */
+ @Nullable
+ private List<MediaSession.QueueItem> getQueueSubset(@NonNull List<MediaSession.QueueItem> items,
+ long startItem, long endItem) {
+ if (endItem > items.size()) {
+ endItem = items.size() - 1;
+ }
+ if (startItem > Integer.MAX_VALUE) {
+ startItem = Integer.MAX_VALUE;
+ }
+ try {
+ List<MediaSession.QueueItem> selected =
+ items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1));
+ if (selected.isEmpty()) {
+ Log.i(TAG, "itemsSubList is empty.");
+ return null;
+ }
+ return selected;
+ } catch (IndexOutOfBoundsException ex) {
+ Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid");
+ } catch (IllegalArgumentException ex) {
+ Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")");
+ }
+ return null;
+ }
+
+ /*
+ * helper method to filter required attibutes before sending GetFolderItems
+ * response
+ */
+ private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
+ @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
+ @NonNull MediaController mediaController) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " + endItem);
+ }
+
+ List<MediaSession.QueueItem> resultItems = getQueueSubset(items, startItem, endItem);
+ /* check for index out of bound errors */
+ if (resultItems == null) {
+ Log.w(TAG, "getFolderItemsFilterAttr: resultItems is empty");
+ mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
+ return;
+ }
+
+ FolderItemsData folderDataNative = new FolderItemsData(resultItems.size());
+
+ /* variables to accumulate attrs */
+ ArrayList<String> attrArray = new ArrayList<String>();
+ ArrayList<Integer> attrId = new ArrayList<Integer>();
+
+ for (int itemIndex = 0; itemIndex < resultItems.size(); itemIndex++) {
+ MediaSession.QueueItem item = resultItems.get(itemIndex);
+ // get the queue id
+ long qid = item.getQueueId();
+ byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
+
+ // get the array of uid from 2d to array 1D array
+ for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
+ folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
+ }
+
+ /* Set display name for current item */
+ folderDataNative.mDisplayNames[itemIndex] =
+ getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController);
+
+ int maxAttributesRequested = 0;
+ boolean isAllAttribRequested = false;
+ /* check if remote requested for attributes */
+ if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
+ int attrCnt = 0;
+
+ /* add requested attr ids to a temp array */
+ if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
+ isAllAttribRequested = true;
+ maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
+ } else {
+ /* get only the requested attribute ids from the request */
+ maxAttributesRequested = folderItemsReqObj.mNumAttr;
+ }
+
+ /* lookup and copy values of attributes for ids requested above */
+ for (int idx = 0; idx < maxAttributesRequested; idx++) {
+ /* check if media player provided requested attributes */
+ String value = null;
+
+ int attribId =
+ isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx];
+ value = getAttrValue(attribId, item, mediaController);
+ if (value != null) {
+ attrArray.add(value);
+ attrId.add(attribId);
+ attrCnt++;
+ }
+ }
+ /* add num attr actually received from media player for a particular item */
+ folderDataNative.mAttributesNum[itemIndex] = attrCnt;
+ }
+ }
+
+ /* copy filtered attr ids and attr values to response parameters */
+ if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
+ folderDataNative.mAttrIds = new int[attrId.size()];
+ for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) {
+ folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
+ }
+ folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
+ }
+ for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++) {
+ if (DEBUG) {
+ Log.d(TAG, "folderDataNative.mAttributesNum"
+ + folderDataNative.mAttributesNum[attrIndex] + " attrIndex " + attrIndex);
+ }
+ }
+
+ /* create rsp object and send response to remote device */
+ FolderItemsRsp rspObj =
+ new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, scope,
+ folderDataNative.mNumItems, folderDataNative.mFolderTypes,
+ folderDataNative.mPlayable, folderDataNative.mItemTypes,
+ folderDataNative.mItemUid, folderDataNative.mDisplayNames,
+ folderDataNative.mAttributesNum, folderDataNative.mAttrIds,
+ folderDataNative.mAttrValues);
+ mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
+ }
+
+ private String getAttrValue(int attr, MediaSession.QueueItem item,
+ @Nullable MediaController mediaController) {
+ String attrValue = null;
+ if (item == null) {
+ if (DEBUG) {
+ Log.d(TAG, "getAttrValue received null item");
+ }
+ return null;
+ }
+ try {
+ MediaDescription desc = item.getDescription();
+ Bundle extras = desc.getExtras();
+ boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController);
+ MediaMetadata data = null;
+ if (isCurrentTrack) {
+ if (DEBUG) {
+ Log.d(TAG, "getAttrValue: item is active, using current data");
+ }
+ data = mediaController.getMetadata();
+ if (data == null) {
+ Log.e(TAG, "getMetadata didn't give us any metadata for the current track");
+ }
+ }
+
+ if (data == null) {
+ // TODO: This code can be removed when b/63117921 is resolved
+ data = (MediaMetadata) extras.get(GPM_BUNDLE_METADATA_KEY);
+ extras = null; // We no longer need the data in here
+ }
+
+ extras = fillBundle(data, extras);
+
+ if (DEBUG) {
+ Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
+ }
+ switch (attr) {
+ case AvrcpConstants.ATTRID_TITLE:
+ /* Title is mandatory attribute */
+ if (isCurrentTrack) {
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE);
+ } else {
+ attrValue = desc.getTitle().toString();
+ }
+ break;
+
+ case AvrcpConstants.ATTRID_ARTIST:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
+ break;
+
+ case AvrcpConstants.ATTRID_ALBUM:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
+ break;
+
+ case AvrcpConstants.ATTRID_TRACK_NUM:
+ attrValue =
+ Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
+ break;
+
+ case AvrcpConstants.ATTRID_NUM_TRACKS:
+ attrValue =
+ Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
+ break;
+
+ case AvrcpConstants.ATTRID_GENRE:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
+ break;
+
+ case AvrcpConstants.ATTRID_PLAY_TIME:
+ attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION));
+ break;
+
+ case AvrcpConstants.ATTRID_COVER_ART:
+ attrValue = Avrcp_ext.getImgHandleFromTitle(desc.getTitle().toString());
+ break;
+
+ default:
+ Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
+ return null;
+ }
+ } catch (NullPointerException ex) {
+ Log.w(TAG, "getAttrValue: attr id not found in result");
+ /* checking if attribute is title, then it is mandatory and cannot send null */
+ if (attr == AvrcpConstants.ATTRID_TITLE) {
+ attrValue = "<Unknown Title>";
+ } else {
+ return null;
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id: " + attr);
+ }
+ return attrValue;
+ }
+
+ private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
+ MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
+ /* Response parameters */
+ int[] attrIds = null; /* array of attr ids */
+ String[] attrValues = null; /* array of attr values */
+
+ /* variables to temperorily add attrs */
+ ArrayList<String> attrArray = new ArrayList<String>();
+ ArrayList<Integer> attrId = new ArrayList<Integer>();
+ ArrayList<Integer> attrTempId = new ArrayList<Integer>();
+
+ /* check if remote device has requested for attributes */
+ if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
+ if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
+ for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) {
+ attrTempId.add(idx); /* attr id 0x00 is unused */
+ }
+ } else {
+ /* get only the requested attribute ids from the request */
+ for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
+ if (DEBUG) {
+ Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
+ + mItemAttrReqObj.mAttrIDs[idx]);
+ }
+ attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
+ }
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
+ }
+ /* lookup and copy values of attributes for ids requested above */
+ for (int idx = 0; idx < attrTempId.size(); idx++) {
+ /* check if media player provided requested attributes */
+ String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController);
+ if (value != null) {
+ attrArray.add(value);
+ attrId.add(attrTempId.get(idx));
+ }
+ }
+
+ /* copy filtered attr ids and attr values to response parameters */
+ if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
+ attrIds = new int[attrId.size()];
+
+ for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) {
+ attrIds[attrIndex] = attrId.get(attrIndex);
+ }
+
+ attrValues = attrArray.toArray(new String[attrId.size()]);
+
+ /* create rsp object and send response */
+ ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
+ mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
+ return;
+ }
+ }
+
+ private long getActiveQueueItemId(@Nullable MediaController controller) {
+ if (controller == null) {
+ return MediaSession.QueueItem.UNKNOWN_ID;
+ }
+ PlaybackState state = controller.getPlaybackState();
+ if (state == null || state.getState() == PlaybackState.STATE_NONE) {
+ return MediaSession.QueueItem.UNKNOWN_ID;
+ }
+ long qid = state.getActiveQueueItemId();
+ if (qid != MediaSession.QueueItem.UNKNOWN_ID) {
+ return qid;
+ }
+ // Check if we're presenting a "one item queue"
+ if (controller.getMetadata() != null) {
+ return SINGLE_QID;
+ }
+ return MediaSession.QueueItem.UNKNOWN_ID;
+ }
+
+ String displayMediaItem(MediaSession.QueueItem item) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("#");
+ sb.append(item.getQueueId());
+ sb.append(": ");
+ sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_TITLE, item, null)));
+ sb.append(" - ");
+ sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ALBUM, item, null)));
+ sb.append(" by ");
+ sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ARTIST, item, null)));
+ sb.append(" (");
+ sb.append(getAttrValue(AvrcpConstants.ATTRID_PLAY_TIME, item, null));
+ sb.append(" ");
+ sb.append(getAttrValue(AvrcpConstants.ATTRID_TRACK_NUM, item, null));
+ sb.append("/");
+ sb.append(getAttrValue(AvrcpConstants.ATTRID_NUM_TRACKS, item, null));
+ sb.append(") ");
+ sb.append(getAttrValue(AvrcpConstants.ATTRID_GENRE, item, null));
+ return sb.toString();
+ }
+
+ public void dump(StringBuilder sb, @Nullable MediaController mediaController) {
+ ProfileService.println(sb, "AddressedPlayer info:");
+ ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent);
+ ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements");
+ long currentQueueId = getActiveQueueItemId(mediaController);
+ for (MediaSession.QueueItem item : mNowPlayingList) {
+ long itemId = item.getQueueId();
+ ProfileService.println(sb,
+ (itemId == currentQueueId ? "*" : " ") + displayMediaItem(item));
+ }
+ }
+}
diff --git a/packages_apps_bluetooth_ext/AvrcpConstants_ext.java b/packages_apps_bluetooth_ext/AvrcpConstants_ext.java
new file mode 100644
index 0000000..82ef436
--- /dev/null
+++ b/packages_apps_bluetooth_ext/AvrcpConstants_ext.java
@@ -0,0 +1,179 @@
+/*
+ * 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.avrcp;
+
+import android.util.Log;
+
+/*************************************************************************************************
+ * Grouped all HAL constants into a file to be consistent with the stack.
+ * Moved the constants used in Avrcp to this new file to be used across multiple files.
+ * Helps in easier modifications and future enhancements in the constants.
+ ************************************************************************************************/
+
+/*
+ * @hide
+ */
+final class AvrcpConstants_ext {
+
+ /* Do not modify without upating the HAL bt_rc.h file */
+ /** Response Error codes **/
+ static final byte RSP_BAD_CMD = 0x00; /* Invalid command */
+ static final byte RSP_BAD_PARAM = 0x01; /* Invalid parameter */
+ static final byte RSP_NOT_FOUND = 0x02; /* Specified parameter is
+ * wrong or not found */
+ static final byte RSP_INTERNAL_ERR = 0x03; /* Internal Error */
+ static final byte RSP_NO_ERROR = 0x04; /* Operation Success */
+ static final byte RSP_UID_CHANGED = 0x05; /* UIDs changed */
+ static final byte RSP_RESERVED = 0x06; /* Reserved */
+ static final byte RSP_INV_DIRN = 0x07; /* Invalid direction */
+ static final byte RSP_INV_DIRECTORY = 0x08; /* Invalid directory */
+ static final byte RSP_INV_ITEM = 0x09; /* Invalid Item */
+ static final byte RSP_INV_SCOPE = 0x0a; /* Invalid scope */
+ static final byte RSP_INV_RANGE = 0x0b; /* Invalid range */
+ static final byte RSP_DIRECTORY = 0x0c; /* UID is a directory */
+ static final byte RSP_MEDIA_IN_USE = 0x0d; /* Media in use */
+ static final byte RSP_PLAY_LIST_FULL = 0x0e; /* Playing list full */
+ static final byte RSP_SRCH_NOT_SPRTD = 0x0f; /* Search not supported */
+ static final byte RSP_SRCH_IN_PROG = 0x10; /* Search in progress */
+ static final byte RSP_INV_PLAYER = 0x11; /* Invalid player */
+ static final byte RSP_PLAY_NOT_BROW = 0x12; /* Player not browsable */
+ static final byte RSP_PLAY_NOT_ADDR = 0x13; /* Player not addressed */
+ static final byte RSP_INV_RESULTS = 0x14; /* Invalid results */
+ static final byte RSP_NO_AVBL_PLAY = 0x15; /* No available players */
+ static final byte RSP_ADDR_PLAY_CHGD = 0x16; /* Addressed player changed */
+
+ /* valid scopes for get_folder_items */
+ static final byte BTRC_SCOPE_PLAYER_LIST = 0x00; /* Media Player List */
+ static final byte BTRC_SCOPE_FILE_SYSTEM = 0x01; /* Virtual File System */
+ static final byte BTRC_SCOPE_SEARCH = 0x02; /* Search */
+ static final byte BTRC_SCOPE_NOW_PLAYING = 0x03; /* Now Playing */
+
+ /* valid directions for change path */
+ static final byte DIR_UP = 0x00;
+ static final byte DIR_DOWN = 0x01;
+
+ /* item type to browse */
+ static final byte BTRC_ITEM_PLAYER = 0x01;
+ static final byte BTRC_ITEM_FOLDER = 0x02;
+ static final byte BTRC_ITEM_MEDIA = 0x03;
+
+ /* valid folder types */
+ static final byte FOLDER_TYPE_MIXED = 0x00;
+ static final byte FOLDER_TYPE_TITLES = 0x01;
+ static final byte FOLDER_TYPE_ALBUMS = 0x02;
+ static final byte FOLDER_TYPE_ARTISTS = 0x03;
+ static final byte FOLDER_TYPE_GENRES = 0x04;
+ static final byte FOLDER_TYPE_PLAYLISTS = 0x05;
+ static final byte FOLDER_TYPE_YEARS = 0x06;
+
+ /* valid playable flags */
+ static final byte ITEM_NOT_PLAYABLE = 0x00;
+ static final byte ITEM_PLAYABLE = 0x01;
+
+ /* valid Attribute ids for media elements */
+ static final int ATTRID_TITLE = 0x01;
+ static final int ATTRID_ARTIST = 0x02;
+ static final int ATTRID_ALBUM = 0x03;
+ static final int ATTRID_TRACK_NUM = 0x04;
+ static final int ATTRID_NUM_TRACKS = 0x05;
+ static final int ATTRID_GENRE = 0x06;
+ static final int ATTRID_PLAY_TIME = 0x07;
+ static final int ATTRID_COVER_ART = 0x08;
+
+ /* constants to send in Track change response */
+ static final byte[] NO_TRACK_SELECTED = {
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF
+ };
+ static final byte[] TRACK_IS_SELECTED = {
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00
+ };
+
+ /* UID size */
+ static final int UID_SIZE = 8;
+
+ static final short DEFAULT_UID_COUNTER = 0x0000;
+
+ /* Bitmask size for Media Players */
+ static final int AVRC_FEATURE_MASK_SIZE = 16;
+
+ /* Maximum attributes for media item */
+ static final int MAX_NUM_ATTR = 8;
+
+ /* notification types for remote device */
+ static final int NOTIFICATION_TYPE_INTERIM = 0;
+ static final int NOTIFICATION_TYPE_CHANGED = 1;
+
+ static final int TRACK_ID_SIZE = 8;
+
+ /* player feature bit mask constants */
+ static final short AVRC_PF_PLAY_BIT_NO = 40;
+ static final short AVRC_PF_STOP_BIT_NO = 41;
+ static final short AVRC_PF_PAUSE_BIT_NO = 42;
+ static final short AVRC_PF_REWIND_BIT_NO = 44;
+ static final short AVRC_PF_FAST_FWD_BIT_NO = 45;
+ static final short AVRC_PF_FORWARD_BIT_NO = 47;
+ static final short AVRC_PF_BACKWARD_BIT_NO = 48;
+ static final short AVRC_PF_ADV_CTRL_BIT_NO = 58;
+ static final short AVRC_PF_BROWSE_BIT_NO = 59;
+ static final short AVRC_PF_ADD2NOWPLAY_BIT_NO = 61;
+ static final short AVRC_PF_UID_UNIQUE_BIT_NO = 62;
+ static final short AVRC_PF_NOW_PLAY_BIT_NO = 65;
+ static final short AVRC_PF_GET_NUM_OF_ITEMS_BIT_NO = 67;
+ static final short AVRC_PF_COVER_ART_BIT_NO = 68;
+ static final byte PLAYER_TYPE_AUDIO = 1;
+ static final int PLAYER_SUBTYPE_NONE = 0;
+
+ // match up with btrc_play_status_t enum of bt_rc.h
+ static final int PLAYSTATUS_STOPPED = 0;
+ static final int PLAYSTATUS_PLAYING = 1;
+ static final int PLAYSTATUS_PAUSED = 2;
+ static final int PLAYSTATUS_FWD_SEEK = 3;
+ static final int PLAYSTATUS_REV_SEEK = 4;
+ static final int PLAYSTATUS_ERROR = 255;
+
+ static final byte NUM_ATTR_ALL = (byte) 0x00;
+ static final byte NUM_ATTR_NONE = (byte) 0xFF;
+
+ static final int KEY_STATE_PRESS = 1;
+ static final int KEY_STATE_RELEASE = 0;
+
+ static final int GET_ATTRIBUTE_IDS = 0;
+ static final int GET_VALUE_IDS = 1;
+ static final int GET_ATTRIBUTE_TEXT = 2;
+ static final int GET_VALUE_TEXT = 3;
+ static final int GET_ATTRIBUTE_VALUES = 4;
+ static final int NOTIFY_ATTRIBUTE_VALUES = 5;
+ static final int SET_ATTRIBUTE_VALUES = 6;
+ static final int GET_INVALID = 0xff;
+
+ public static final String TAG = "Avrcp";
+ public static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+}
diff --git a/packages_apps_bluetooth_ext/AvrcpHelperClasses_ext.java b/packages_apps_bluetooth_ext/AvrcpHelperClasses_ext.java
new file mode 100644
index 0000000..ea6d7b8
--- /dev/null
+++ b/packages_apps_bluetooth_ext/AvrcpHelperClasses_ext.java
@@ -0,0 +1,451 @@
+/*
+ * 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.avrcp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.bluetooth.Utils;
+
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collection;
+
+/*************************************************************************************************
+ * Helper classes used for callback/response of browsing commands:-
+ * 1) To bundle parameters for native callbacks/response.
+ * 2) Stores information of Addressed and Browsed Media Players.
+ ************************************************************************************************/
+
+class AvrcpCmd {
+
+ AvrcpCmd() {}
+
+ /* Helper classes to pass parameters from callbacks to Avrcp handler */
+ class FolderItemsCmd {
+ byte mScope;
+ long mStartItem;
+ long mEndItem;
+ byte mNumAttr;
+ int[] mAttrIDs;
+ public byte[] mAddress;
+
+ FolderItemsCmd(byte[] address, byte scope, long startItem, long endItem, byte numAttr,
+ int[] attrIds) {
+ mAddress = address;
+ this.mScope = scope;
+ this.mStartItem = startItem;
+ this.mEndItem = endItem;
+ this.mNumAttr = numAttr;
+ this.mAttrIDs = attrIds;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[FolderItemCmd: scope " + mScope);
+ sb.append(" start " + mStartItem);
+ sb.append(" end " + mEndItem);
+ sb.append(" numAttr " + mNumAttr);
+ sb.append(" attrs: ");
+ for (int i = 0; i < mNumAttr; i++) {
+ sb.append(mAttrIDs[i] + " ");
+ }
+ return sb.toString();
+ }
+ }
+
+ class ItemAttrCmd {
+ byte mScope;
+ byte[] mUid;
+ int mUidCounter;
+ byte mNumAttr;
+ int[] mAttrIDs;
+ public byte[] mAddress;
+
+ ItemAttrCmd(byte[] address, byte scope, byte[] uid, int uidCounter, byte numAttr,
+ int[] attrIDs) {
+ mAddress = address;
+ mScope = scope;
+ mUid = uid;
+ mUidCounter = uidCounter;
+ mNumAttr = numAttr;
+ mAttrIDs = attrIDs;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[ItemAttrCmd: scope " + mScope);
+ sb.append(" uid " + Utils.byteArrayToString(mUid));
+ sb.append(" numAttr " + mNumAttr);
+ sb.append(" attrs: ");
+ for (int i = 0; i < mNumAttr; i++) {
+ sb.append(mAttrIDs[i] + " ");
+ }
+ return sb.toString();
+ }
+ }
+
+ class ElementAttrCmd {
+ byte mNumAttr;
+ int[] mAttrIDs;
+ public byte[] mAddress;
+
+ ElementAttrCmd(byte[] address, byte numAttr, int[] attrIDs) {
+ mAddress = address;
+ mNumAttr = numAttr;
+ mAttrIDs = attrIDs;
+ }
+ }
+}
+
+/* Helper classes to pass parameters to native response */
+class MediaPlayerListRsp {
+ byte mStatus;
+ short mUIDCounter;
+ byte mItemType;
+ int[] mPlayerIds;
+ byte[] mPlayerTypes;
+ int[] mPlayerSubTypes;
+ byte[] mPlayStatusValues;
+ short[] mFeatureBitMaskValues;
+ String[] mPlayerNameList;
+ int mNumItems;
+
+ MediaPlayerListRsp(byte status, short uidCounter, int numItems, byte itemType, int[] playerIds,
+ byte[] playerTypes, int[] playerSubTypes, byte[] playStatusValues,
+ short[] featureBitMaskValues, String[] playerNameList) {
+ this.mStatus = status;
+ this.mUIDCounter = uidCounter;
+ this.mNumItems = numItems;
+ this.mItemType = itemType;
+ this.mPlayerIds = playerIds;
+ this.mPlayerTypes = playerTypes;
+ this.mPlayerSubTypes = new int[numItems];
+ this.mPlayerSubTypes = playerSubTypes;
+ this.mPlayStatusValues = new byte[numItems];
+ this.mPlayStatusValues = playStatusValues;
+ int bitMaskSize = AvrcpConstants.AVRC_FEATURE_MASK_SIZE;
+ this.mFeatureBitMaskValues = new short[numItems * bitMaskSize];
+ for (int bitMaskIndex = 0; bitMaskIndex < (numItems * bitMaskSize); bitMaskIndex++) {
+ this.mFeatureBitMaskValues[bitMaskIndex] = featureBitMaskValues[bitMaskIndex];
+ }
+ this.mPlayerNameList = playerNameList;
+ }
+}
+
+class FolderItemsRsp {
+ byte mStatus;
+ short mUIDCounter;
+ byte mScope;
+ int mNumItems;
+ byte[] mFolderTypes;
+ byte[] mPlayable;
+ byte[] mItemTypes;
+ byte[] mItemUid;
+ String[] mDisplayNames; /* display name of the item. Eg: Folder name or song name */
+ int[] mAttributesNum;
+ int[] mAttrIds;
+ String[] mAttrValues;
+
+ FolderItemsRsp(byte status, short uidCounter, byte scope, int numItems, byte[] folderTypes,
+ byte[] playable, byte[] itemTypes, byte[] itemsUid, String[] displayNameArray,
+ int[] attributesNum, int[] attrIds, String[] attrValues) {
+ this.mStatus = status;
+ this.mUIDCounter = uidCounter;
+ this.mScope = scope;
+ this.mNumItems = numItems;
+ this.mFolderTypes = folderTypes;
+ this.mPlayable = playable;
+ this.mItemTypes = itemTypes;
+ this.mItemUid = itemsUid;
+ this.mDisplayNames = displayNameArray;
+ this.mAttributesNum = attributesNum;
+ this.mAttrIds = attrIds;
+ this.mAttrValues = attrValues;
+ }
+}
+
+class ItemAttrRsp {
+ byte mStatus;
+ byte mNumAttr;
+ int[] mAttributesIds;
+ String[] mAttributesArray;
+
+ ItemAttrRsp(byte status, int[] attributesIds, String[] attributesArray) {
+ mStatus = status;
+ mNumAttr = (byte) attributesIds.length;
+ mAttributesIds = attributesIds;
+ mAttributesArray = attributesArray;
+ }
+}
+
+/* stores information of Media Players in the system */
+class MediaPlayerInfo {
+
+ private byte mMajorType;
+ private int mSubType;
+ private byte mPlayStatus;
+ private short[] mFeatureBitMask;
+ @NonNull private String mPackageName;
+ @NonNull private String mDisplayableName;
+ @Nullable private MediaController mMediaController;
+
+ MediaPlayerInfo(@Nullable MediaController controller, byte majorType, int subType,
+ byte playStatus, short[] featureBitMask, @NonNull String packageName,
+ @Nullable String displayableName) {
+ this.setMajorType(majorType);
+ this.setSubType(subType);
+ this.mPlayStatus = playStatus;
+ // store a copy the FeatureBitMask array
+ this.mFeatureBitMask = Arrays.copyOf(featureBitMask, featureBitMask.length);
+ Arrays.sort(this.mFeatureBitMask);
+ this.setPackageName(packageName);
+ this.setDisplayableName(displayableName);
+ this.setMediaController(controller);
+ }
+
+ /* getters and setters */
+ byte getPlayStatus() {
+ return mPlayStatus;
+ }
+
+ void setPlayStatus(byte playStatus) {
+ this.mPlayStatus = playStatus;
+ }
+
+ MediaController getMediaController() {
+ return mMediaController;
+ }
+
+ void setMediaController(MediaController mediaController) {
+ if (mediaController != null) {
+ this.mPackageName = mediaController.getPackageName();
+ }
+ this.mMediaController = mediaController;
+ }
+
+ void setPackageName(@NonNull String name) {
+ // Controller determines package name when it is set.
+ if (mMediaController != null) {
+ return;
+ }
+ this.mPackageName = name;
+ }
+
+ String getPackageName() {
+ if (mMediaController != null) {
+ return mMediaController.getPackageName();
+ } else if (mPackageName != null) {
+ return mPackageName;
+ }
+ return null;
+ }
+
+ byte getMajorType() {
+ return mMajorType;
+ }
+
+ void setMajorType(byte majorType) {
+ this.mMajorType = majorType;
+ }
+
+ int getSubType() {
+ return mSubType;
+ }
+
+ void setSubType(int subType) {
+ this.mSubType = subType;
+ }
+
+ String getDisplayableName() {
+ return mDisplayableName;
+ }
+
+ void setDisplayableName(@Nullable String displayableName) {
+ if (displayableName == null) {
+ displayableName = "";
+ }
+ this.mDisplayableName = displayableName;
+ }
+
+ short[] getFeatureBitMask() {
+ return mFeatureBitMask;
+ }
+
+ void setFeatureBitMask(short[] featureBitMask) {
+ synchronized (this) {
+ this.mFeatureBitMask = Arrays.copyOf(featureBitMask, featureBitMask.length);
+ Arrays.sort(this.mFeatureBitMask);
+ }
+ }
+
+ boolean isBrowseSupported() {
+ synchronized (this) {
+ if (this.mFeatureBitMask == null) {
+ return false;
+ }
+ for (short bit : this.mFeatureBitMask) {
+ if (bit == AvrcpConstants.AVRC_PF_BROWSE_BIT_NO) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Tests if the view of this player presented to the controller is different enough to
+ * justify sending an Available Players Changed update */
+ public boolean equalView(MediaPlayerInfo other) {
+ return (this.mMajorType == other.getMajorType()) && (this.mSubType == other.getSubType())
+ && Arrays.equals(this.mFeatureBitMask, other.getFeatureBitMask())
+ && this.mDisplayableName.equals(other.getDisplayableName());
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("MediaPlayerInfo ");
+ sb.append(getPackageName());
+ sb.append(" (as '" + getDisplayableName() + "')");
+ sb.append(" Type = " + getMajorType());
+ sb.append(", SubType = " + getSubType());
+ sb.append(", Status = " + mPlayStatus);
+ sb.append(" Feature Bits [");
+ short[] bits = getFeatureBitMask();
+ for (int i = 0; i < bits.length; i++) {
+ if (i != 0) {
+ sb.append(" ");
+ }
+ sb.append(bits[i]);
+ }
+ sb.append("] Controller: ");
+ sb.append(getMediaController());
+ return sb.toString();
+ }
+}
+
+/* stores information for browsable Media Players available in the system */
+class BrowsePlayerInfo {
+ public String packageName;
+ public String displayableName;
+ public String serviceClass;
+
+ BrowsePlayerInfo(String packageName, String displayableName, String serviceClass) {
+ this.packageName = packageName;
+ this.displayableName = displayableName;
+ this.serviceClass = serviceClass;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("BrowsePlayerInfo ");
+ sb.append(packageName);
+ sb.append(" ( as '" + displayableName + "')");
+ sb.append(" service " + serviceClass);
+ return sb.toString();
+ }
+}
+
+class FolderItemsData {
+ /* initialize sizes for rsp parameters */ int mNumItems;
+ int[] mAttributesNum;
+ byte[] mFolderTypes;
+ byte[] mItemTypes;
+ byte[] mPlayable;
+ byte[] mItemUid;
+ String[] mDisplayNames;
+ int[] mAttrIds;
+ String[] mAttrValues;
+ int mAttrCounter;
+
+ FolderItemsData(int size) {
+ mNumItems = size;
+ mAttributesNum = new int[size];
+
+ mFolderTypes = new byte[size]; /* folderTypes */
+ mItemTypes = new byte[size]; /* folder or media item */
+ mPlayable = new byte[size];
+ Arrays.fill(mFolderTypes, AvrcpConstants.FOLDER_TYPE_MIXED);
+ Arrays.fill(mItemTypes, AvrcpConstants.BTRC_ITEM_MEDIA);
+ Arrays.fill(mPlayable, AvrcpConstants.ITEM_PLAYABLE);
+
+ mItemUid = new byte[size * AvrcpConstants.UID_SIZE];
+ mDisplayNames = new String[size];
+
+ mAttrIds = null; /* array of attr ids */
+ mAttrValues = null; /* array of attr values */
+ }
+}
+
+/** A queue that evicts the first element when you add an element to the end when it reaches a
+ * maximum size.
+ * This is useful for keeping a FIFO queue of items where the items drop off the front, i.e. a log
+ * with a maximum size.
+ */
+class EvictingQueue<E> extends ArrayDeque<E> {
+ private int mMaxSize;
+
+ EvictingQueue(int maxSize) {
+ super();
+ mMaxSize = maxSize;
+ }
+
+ EvictingQueue(int maxSize, int initialElements) {
+ super(initialElements);
+ mMaxSize = maxSize;
+ }
+
+ EvictingQueue(int maxSize, Collection<? extends E> c) {
+ super(c);
+ mMaxSize = maxSize;
+ }
+
+ @Override
+ public void addFirst(E e) {
+ if (super.size() == mMaxSize) {
+ return;
+ }
+ super.addFirst(e);
+ }
+
+ @Override
+ public void addLast(E e) {
+ if (super.size() == mMaxSize) {
+ super.remove();
+ }
+ super.addLast(e);
+ }
+
+ @Override
+ public boolean offerFirst(E e) {
+ if (super.size() == mMaxSize) {
+ return false;
+ }
+ return super.offerFirst(e);
+ }
+
+ @Override
+ public boolean offerLast(E e) {
+ if (super.size() == mMaxSize) {
+ super.remove();
+ }
+ return super.offerLast(e);
+ }
+}
diff --git a/packages_apps_bluetooth_ext/AvrcpMediaRspInterface_ext.java b/packages_apps_bluetooth_ext/AvrcpMediaRspInterface_ext.java
new file mode 100644
index 0000000..4be98d3
--- /dev/null
+++ b/packages_apps_bluetooth_ext/AvrcpMediaRspInterface_ext.java
@@ -0,0 +1,53 @@
+/*
+ * 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.avrcp;
+
+
+/*************************************************************************************************
+ * Interface for classes which handle callbacks from AvrcpMediaManager.
+ * These callbacks should map to native responses and used to communicate with the native layer.
+ ************************************************************************************************/
+
+public interface AvrcpMediaRspInterface_ext {
+ void setAddrPlayerRsp(byte[] address, int rspStatus);
+
+ void setBrowsedPlayerRsp(byte[] address, int rspStatus, byte depth, int numItems,
+ String[] textArray);
+
+ void mediaPlayerListRsp(byte[] address, int rspStatus, MediaPlayerListRsp rspObj);
+
+ void folderItemsRsp(byte[] address, int rspStatus, FolderItemsRsp rspObj);
+
+ void changePathRsp(byte[] address, int rspStatus, int numItems);
+
+ void getItemAttrRsp(byte[] address, int rspStatus, ItemAttrRsp rspObj);
+
+ void playItemRsp(byte[] address, int rspStatus);
+
+ void getTotalNumOfItemsRsp(byte[] address, int rspStatus, int uidCounter, int numItems);
+
+ void addrPlayerChangedRsp(int type, int playerId, int uidCounter);
+
+ void avalPlayerChangedRsp(byte[] address, int type);
+
+ void uidsChangedRsp(int type);
+
+ void nowPlayingChangedRsp(int type);
+
+ void trackChangedRsp(int type, byte[] uid);
+}
+
diff --git a/packages_apps_bluetooth_ext/BrowsedMediaPlayer_ext.java b/packages_apps_bluetooth_ext/BrowsedMediaPlayer_ext.java
new file mode 100644
index 0000000..d6d4b25
--- /dev/null
+++ b/packages_apps_bluetooth_ext/BrowsedMediaPlayer_ext.java
@@ -0,0 +1,975 @@
+/*
+ * 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.avrcp;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.browse.MediaBrowser;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Stack;
+
+/*************************************************************************************************
+ * Provides functionality required for Browsed Media Player like browsing Virtual File System, get
+ * Item Attributes, play item from the file system, etc.
+ * Acts as an Interface to communicate with Media Browsing APIs for browsing FileSystem.
+ ************************************************************************************************/
+
+class BrowsedMediaPlayer_ext {
+ private static final boolean DEBUG = true;
+ private static final String TAG = "BrowsedMediaPlayer_ext";
+
+ /* connection state with MediaBrowseService */
+ private static final int DISCONNECTED = 0;
+ private static final int CONNECTED = 1;
+ private static final int SUSPENDED = 2;
+
+ private static final int BROWSED_ITEM_ID_INDEX = 2;
+
+ private static final String[] ROOT_FOLDER = {"root"};
+
+ /* package and service name of target Media Player which is set for browsing */
+ private String mPackageName;
+ private String mConnectingPackageName;
+ private String mClassName;
+ private Context mContext;
+ private AvrcpMediaRspInterface mMediaInterface;
+ private byte[] mBDAddr;
+
+ /* Object used to connect to MediaBrowseService of Media Player */
+ private MediaBrowser mMediaBrowser = null;
+ private MediaController mMediaController = null;
+
+ /* The mediaId to be used for subscribing for children using the MediaBrowser */
+ private String mMediaId = null;
+ private String mRootFolderUid = null;
+ private int mConnState = DISCONNECTED;
+
+ /* stores the path trail during changePath */
+ private Stack<String> mPathStack = null;
+
+ /* Number of items in current folder */
+ private int mCurrFolderNumItems = 0;
+
+ /* store mapping between uid(Avrcp) and mediaId(Media Player) for Media Item */
+ private HashMap<Integer, String> mMediaHmap = new HashMap<Integer, String>();
+
+ /* store mapping between uid(Avrcp) and mediaId(Media Player) for Folder Item */
+ private HashMap<Integer, String> mFolderHmap = new HashMap<Integer, String>();
+
+ /* command objects from avrcp handler */
+ private AvrcpCmd.FolderItemsCmd mFolderItemsReqObj;
+
+ /* store result of getfolderitems with scope="vfs" */
+ private List<MediaBrowser.MediaItem> mFolderItems = null;
+
+ /* Connection state callback handler */
+ class MediaConnectionCallback extends MediaBrowser.ConnectionCallback {
+ private String mCallbackPackageName;
+ private MediaBrowser mBrowser;
+
+ MediaConnectionCallback(String packageName) {
+ this.mCallbackPackageName = packageName;
+ }
+
+ public void setBrowser(MediaBrowser b) {
+ mBrowser = b;
+ }
+
+ @Override
+ public void onConnected() {
+ mConnState = CONNECTED;
+ if (DEBUG) {
+ Log.d(TAG, "mediaBrowser CONNECTED to " + mPackageName);
+ }
+ /* perform init tasks and set player as browsed player on successful connection */
+ onBrowseConnect(mCallbackPackageName, mBrowser);
+
+ // Remove what could be a circular dependency causing GC to never happen on this object
+ mBrowser = null;
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ mConnState = DISCONNECTED;
+ // Remove what could be a circular dependency causing GC to never happen on this object
+ mBrowser = null;
+ Log.e(TAG, "mediaBrowser Connection failed with " + mPackageName
+ + ", Sending fail response!");
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR,
+ (byte) 0x00, 0, null);
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ mBrowser = null;
+ mConnState = SUSPENDED;
+ Log.e(TAG, "mediaBrowser SUSPENDED connection with " + mPackageName);
+ }
+ }
+
+ /* Subscription callback handler. Subscribe to a folder to get its contents */
+ private MediaBrowser.SubscriptionCallback mFolderItemsCb =
+ new MediaBrowser.SubscriptionCallback() {
+
+ @Override
+ public void onChildrenLoaded(String parentId,
+ List<MediaBrowser.MediaItem> children) {
+ if (DEBUG) {
+ Log.d(TAG, "OnChildren Loaded folder items: childrens= " + children.size());
+ }
+
+ /*
+ * cache current folder items and send as rsp when remote requests
+ * get_folder_items (scope = vfs)
+ */
+ if (mFolderItems == null) {
+ if (DEBUG) {
+ Log.d(TAG, "sending setbrowsed player rsp");
+ }
+ mFolderItems = children;
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR,
+ (byte) 0x00, children.size(), ROOT_FOLDER);
+ } else {
+ mFolderItems = children;
+ mCurrFolderNumItems = mFolderItems.size();
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR,
+ mCurrFolderNumItems);
+ }
+ refreshFolderItems(mFolderItems);
+ mMediaBrowser.unsubscribe(parentId);
+ }
+
+ /* UID is invalid */
+ @Override
+ public void onError(String id) {
+ Log.e(TAG, "set browsed player rsp. Could not get root folder items");
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR,
+ (byte) 0x00, 0, null);
+ }
+ };
+
+ /* callback from media player in response to getitemAttr request */
+ private class ItemAttribSubscriber extends MediaBrowser.SubscriptionCallback {
+ private String mMediaId;
+ private AvrcpCmd.ItemAttrCmd mAttrReq;
+
+ ItemAttribSubscriber(@NonNull AvrcpCmd.ItemAttrCmd attrReq, @NonNull String mediaId) {
+ mAttrReq = attrReq;
+ mMediaId = mediaId;
+ }
+
+ @Override
+ public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
+ String logprefix = "ItemAttribSubscriber(" + mMediaId + "): ";
+ if (DEBUG) {
+ Log.d(TAG, logprefix + "OnChildren Loaded");
+ }
+ int status = AvrcpConstants.RSP_INV_ITEM;
+
+ if (children == null) {
+ Log.w(TAG, logprefix + "children list is null parentId: " + parentId);
+ } else {
+ /* find the item in the folder */
+ for (MediaBrowser.MediaItem item : children) {
+ if (item.getMediaId().equals(mMediaId)) {
+ if (DEBUG) {
+ Log.d(TAG, logprefix + "found item");
+ }
+ getItemAttrFilterAttr(item);
+ status = AvrcpConstants.RSP_NO_ERROR;
+ break;
+ }
+ }
+ }
+ /* Send only error from here, in case of success, getItemAttrFilterAttr sends */
+ if (status != AvrcpConstants.RSP_NO_ERROR) {
+ Log.e(TAG, logprefix + "not able to find item from " + parentId);
+ mMediaInterface.getItemAttrRsp(mBDAddr, status, null);
+ }
+ mMediaBrowser.unsubscribe(parentId);
+ }
+
+ @Override
+ public void onError(String id) {
+ Log.e(TAG, "Could not get attributes from media player id: " + id);
+ mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
+ }
+
+ /* helper method to filter required attibuteand send GetItemAttr response */
+ private void getItemAttrFilterAttr(@NonNull MediaBrowser.MediaItem mediaItem) {
+ /* Response parameters */
+ int[] attrIds = null; /* array of attr ids */
+ String[] attrValues = null; /* array of attr values */
+
+ /* variables to temperorily add attrs */
+ ArrayList<Integer> attrIdArray = new ArrayList<Integer>();
+ ArrayList<String> attrValueArray = new ArrayList<String>();
+ ArrayList<Integer> attrReqIds = new ArrayList<Integer>();
+
+ if (mAttrReq.mNumAttr == AvrcpConstants.NUM_ATTR_NONE) {
+ // Note(jamuraa): the stack should never send this, remove?
+ Log.i(TAG, "getItemAttrFilterAttr: No attributes requested");
+ mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_BAD_PARAM, null);
+ return;
+ }
+
+ /* check if remote device has requested all attributes */
+ if (mAttrReq.mNumAttr == AvrcpConstants.NUM_ATTR_ALL
+ || mAttrReq.mNumAttr == AvrcpConstants.MAX_NUM_ATTR) {
+ for (int idx = 1; idx <= AvrcpConstants.MAX_NUM_ATTR; idx++) {
+ attrReqIds.add(idx); /* attr id 0x00 is unused */
+ }
+ } else {
+ /* get only the requested attribute ids from the request */
+ for (int idx = 0; idx < mAttrReq.mNumAttr; idx++) {
+ attrReqIds.add(mAttrReq.mAttrIDs[idx]);
+ }
+ }
+
+ /* lookup and copy values of attributes for ids requested above */
+ for (int attrId : attrReqIds) {
+ /* check if media player provided requested attributes */
+ String value = getAttrValue(attrId, mediaItem);
+ if (value != null) {
+ attrIdArray.add(attrId);
+ attrValueArray.add(value);
+ }
+ }
+
+ /* copy filtered attr ids and attr values to response parameters */
+ attrIds = new int[attrIdArray.size()];
+ for (int i = 0; i < attrIdArray.size(); i++) {
+ attrIds[i] = attrIdArray.get(i);
+ }
+
+ attrValues = attrValueArray.toArray(new String[attrIdArray.size()]);
+
+ /* create rsp object and send response */
+ ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
+ mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
+ }
+ }
+
+ /* Constructor */
+ BrowsedMediaPlayer(byte[] address, Context context,
+ AvrcpMediaRspInterface mAvrcpMediaRspInterface) {
+ mContext = context;
+ mMediaInterface = mAvrcpMediaRspInterface;
+ mBDAddr = address;
+ }
+
+ /* initialize mediacontroller in order to communicate with media player. */
+ private void onBrowseConnect(String connectedPackage, MediaBrowser browser) {
+ if (!connectedPackage.equals(mConnectingPackageName)) {
+ Log.w(TAG, "onBrowseConnect: recieved callback for package" + mConnectingPackageName +
+ "we aren't connecting to " + connectedPackage);
+ mMediaInterface.setBrowsedPlayerRsp(
+ mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0x00, 0, null);
+ return;
+ }
+ mConnectingPackageName = null;
+
+ if (browser == null) {
+ Log.e(TAG, "onBrowseConnect: received a null browser for " + connectedPackage);
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR,
+ (byte) 0x00, 0, null);
+ return;
+ }
+
+ MediaSession.Token token = null;
+ try {
+ if (!browser.isConnected()) {
+ Log.e(TAG, "setBrowsedPlayer: " + mPackageName + "not connected");
+ } else if ((token = browser.getSessionToken()) == null) {
+ Log.e(TAG, "setBrowsedPlayer: " + mPackageName + "no Session token");
+ } else {
+ /* update to the new MediaBrowser */
+ if (mMediaBrowser != null) {
+ mMediaBrowser.disconnect();
+ }
+ mMediaBrowser = browser;
+ mPackageName = connectedPackage;
+
+ /* get rootfolder uid from media player */
+ if (mMediaId == null) {
+ mMediaId = mMediaBrowser.getRoot();
+ Log.d(TAG, "media browser root = " + mMediaId);
+
+ if (mMediaId == null || mMediaId.length() == 0) {
+ Log.e(TAG, "onBrowseConnect: root value is empty or null");
+ mMediaInterface.setBrowsedPlayerRsp(
+ mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0x00, 0, null);
+ return;
+ }
+
+ /*
+ * assuming that root folder uid will not change on uids changed
+ */
+ mRootFolderUid = mMediaId;
+ /* store root folder uid to stack */
+ mPathStack.push(mMediaId);
+ /* get root folder items */
+ mMediaBrowser.subscribe(mRootFolderUid, mFolderItemsCb);
+ }
+
+ mMediaController = MediaControllerFactory.make(mContext, token);
+ return;
+ }
+ } catch (NullPointerException ex) {
+ Log.e(TAG, "setBrowsedPlayer : Null pointer during init");
+ ex.printStackTrace();
+ }
+
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0x00,
+ 0, null);
+ }
+
+ public void setBrowsed(String packageName, String cls) {
+ Log.w(TAG, "!! In setBrowse function !!" + mFolderItems);
+ if ((mPackageName != null && packageName != null
+ && !mPackageName.equals(packageName)) || (mFolderItems == null)) {
+ Log.d(TAG, "setBrowse for packageName = " + packageName);
+ mConnectingPackageName = packageName;
+ mPackageName = packageName;
+ mClassName = cls;
+
+ /* cleanup variables from previous browsed calls */
+ mFolderItems = null;
+ mMediaId = null;
+ mRootFolderUid = null;
+ /*
+ * create stack to store the navigation trail (current folder ID). This
+ * will be required while navigating up the folder
+ */
+ mPathStack = new Stack<String>();
+ /* Bind to MediaBrowseService of MediaPlayer */
+ MediaConnectionCallback callback = new MediaConnectionCallback(packageName);
+ MediaBrowser tempBrowser = new MediaBrowser(
+ mContext, new ComponentName(packageName, mClassName), callback, null);
+ callback.setBrowser(tempBrowser);
+ tempBrowser.connect();
+ } else if (mFolderItems != null) {
+ mPackageName = packageName;
+ mClassName = cls;
+ int rsp_status = AvrcpConstants.RSP_NO_ERROR;
+ int folder_depth = (mPathStack.size() > 0) ? (mPathStack.size() - 1) : 0;
+ if (!mPathStack.empty()) {
+ Log.d(TAG, "~~current Path = " + mPathStack.peek());
+ if (mPathStack.size() > 1) {
+ String top = mPathStack.peek();
+ mPathStack.pop();
+ String path = mPathStack.peek();
+ mPathStack.push(top);
+ String [] ExternalPath = path.split("/");
+ String [] folderPath = new String[ExternalPath.length - 1];
+ for (int i = 0; i < (ExternalPath.length - 1); i++) {
+ folderPath[i] = ExternalPath[i + 1];
+ Log.d(TAG,"folderPath[" + i + "] = " + folderPath[i]);
+ }
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, rsp_status,
+ (byte)folder_depth, mFolderItems.size(), folderPath);
+ } else if (mPathStack.size() == 1) {
+ Log.d(TAG, "On root send SetBrowse response with root properties");
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, rsp_status, (byte)folder_depth,
+ mFolderItems.size(), ROOT_FOLDER);
+ }
+ } else {
+ Log.e(TAG, "Path Stack empty sending internal error !!!");
+ rsp_status = AvrcpConstants.RSP_INTERNAL_ERR;
+ mMediaInterface.setBrowsedPlayerRsp(mBDAddr, rsp_status, (byte)0x00, 0, null);
+ }
+ Log.d(TAG, "send setbrowse rsp status=" + rsp_status + " folder_depth=" + folder_depth);
+ }
+ }
+
+ /* called when connection to media player is closed */
+ public void cleanup() {
+ if (DEBUG) {
+ Log.d(TAG, "cleanup");
+ }
+
+ if (mConnState != DISCONNECTED) {
+ if (mMediaBrowser != null) mMediaBrowser.disconnect();
+ }
+
+ mMediaHmap = null;
+ mFolderHmap = null;
+ mMediaController = null;
+ mMediaBrowser = null;
+ mPathStack = null;
+ }
+
+ public boolean isPlayerConnected() {
+ if (mMediaBrowser == null) {
+ if (DEBUG) {
+ Log.d(TAG, "isPlayerConnected: mMediaBrowser = null!");
+ }
+ return false;
+ }
+
+ return mMediaBrowser.isConnected();
+ }
+
+ /* returns number of items in new path as reponse */
+ public void changePath(byte[] folderUid, byte direction) {
+ if (DEBUG) {
+ Log.d(TAG, "changePath.direction = " + direction);
+ }
+ String newPath = "";
+
+ if (!isPlayerConnected()) {
+ Log.w(TAG, "changePath: disconnected from player service, sending internal error");
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0);
+ return;
+ }
+
+ if (mMediaBrowser == null) {
+ Log.e(TAG, "Media browser is null, sending internal error");
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0);
+ return;
+ }
+
+ /* check direction and change the path */
+ if (direction == AvrcpConstants.DIR_DOWN) { /* move down */
+ if ((newPath = byteToStringFolder(folderUid)) == null) {
+ Log.e(TAG, "Could not get media item from folder Uid, sending err response");
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM, 0);
+ } else if (!isBrowsableFolderDn(newPath)) {
+ /* new path is not browsable */
+ Log.e(TAG, "ItemUid received from changePath cmd is not browsable");
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRECTORY, 0);
+ } else if (mPathStack.peek().equals(newPath)) {
+ /* new_folder is same as current folder */
+ Log.e(TAG, "new_folder is same as current folder, Invalid direction!");
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0);
+ } else {
+ mMediaBrowser.subscribe(newPath, mFolderItemsCb);
+ /* assume that call is success and update stack with new folder path */
+ mPathStack.push(newPath);
+ }
+ } else if (direction == AvrcpConstants.DIR_UP) { /* move up */
+ if (!isBrowsableFolderUp()) {
+ /* Already on the root, cannot allow up: PTS: test case TC_TG_MCN_CB_BI_02_C
+ * This is required, otherwise some CT will keep on sending change path up
+ * until they receive error */
+ Log.w(TAG, "Cannot go up from now, already in the root, Invalid direction!");
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0);
+ } else {
+ /* move folder up */
+ mPathStack.pop();
+ newPath = mPathStack.peek();
+ mMediaBrowser.subscribe(newPath, mFolderItemsCb);
+ }
+ } else { /* invalid direction */
+ Log.w(TAG, "changePath : Invalid direction " + direction);
+ mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0);
+ }
+ }
+
+ public void getItemAttr(AvrcpCmd.ItemAttrCmd itemAttr) {
+ String mediaID;
+ if (DEBUG) {
+ Log.d(TAG, "getItemAttr");
+ }
+
+ /* check if uid is valid by doing a lookup in hashmap */
+ mediaID = byteToStringMedia(itemAttr.mUid);
+ if (mediaID == null) {
+ Log.e(TAG, "uid is invalid");
+ mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM, null);
+ return;
+ }
+
+ /* check scope */
+ if (itemAttr.mScope != AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
+ Log.e(TAG, "invalid scope");
+ mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE, null);
+ return;
+ }
+
+ if (mMediaBrowser == null) {
+ Log.e(TAG, "mMediaBrowser is null");
+ mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
+ return;
+ }
+
+ /* Subscribe to the parent to list items and retrieve the right one */
+ mMediaBrowser.subscribe(mPathStack.peek(), new ItemAttribSubscriber(itemAttr, mediaID));
+ }
+
+ public void getTotalNumOfItems(byte scope) {
+ if (DEBUG) {
+ Log.d(TAG, "getTotalNumOfItems scope = " + scope);
+ }
+ if (scope != AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
+ Log.e(TAG, "getTotalNumOfItems error" + scope);
+ mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE, 0, 0);
+ return;
+ }
+
+ if (mFolderItems == null) {
+ Log.e(TAG, "mFolderItems is null, sending internal error");
+ /* folderitems were not fetched during change path */
+ mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0, 0);
+ return;
+ }
+
+ /* find num items using size of already cached folder items */
+ mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, 0,
+ mFolderItems.size());
+ }
+
+ public void getFolderItemsVFS(AvrcpCmd.FolderItemsCmd reqObj) {
+ if (!isPlayerConnected()) {
+ Log.e(TAG, "unable to connect to media player, sending internal error");
+ /* unable to connect to media player. Send error response to remote device */
+ mMediaInterface.folderItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "getFolderItemsVFS");
+ }
+ mFolderItemsReqObj = reqObj;
+
+ if (mFolderItems == null) {
+ /* Failed to fetch folder items from media player. Send error to remote device */
+ Log.e(TAG, "Failed to fetch folder items during getFolderItemsVFS");
+ mMediaInterface.folderItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
+ return;
+ }
+
+ /* Filter attributes based on the request and send response to remote device */
+ getFolderItemsFilterAttr(mBDAddr, reqObj, mFolderItems,
+ AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM, mFolderItemsReqObj.mStartItem,
+ mFolderItemsReqObj.mEndItem);
+ }
+
+ /* Instructs media player to play particular media item */
+ public void playItem(byte[] uid, byte scope) {
+ String folderUid;
+
+ if (isPlayerConnected()) {
+ /* check if uid is valid */
+ if ((folderUid = byteToStringMedia(uid)) == null) {
+ Log.e(TAG, "uid is invalid!");
+ mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM);
+ return;
+ }
+
+ if (mMediaController != null) {
+ MediaController.TransportControls mediaControllerCntrl =
+ mMediaController.getTransportControls();
+ if (DEBUG) {
+ Log.d(TAG, "Sending playID: " + folderUid);
+ }
+
+ if (scope == AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
+ mediaControllerCntrl.playFromMediaId(folderUid, null);
+ mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR);
+ } else {
+ Log.e(TAG, "playItem received for invalid scope!");
+ mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE);
+ }
+ } else {
+ Log.e(TAG, "mediaController is null");
+ mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR);
+ }
+ } else {
+ Log.e(TAG, "playItem: Not connected to media player");
+ mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR);
+ }
+ }
+
+ /*
+ * helper method to check if startItem and endItem index is with range of
+ * MediaItem list. (Resultset containing all items in current path)
+ */
+ private List<MediaBrowser.MediaItem> checkIndexOutofBounds(byte[] bdaddr,
+ List<MediaBrowser.MediaItem> children, long startItem, long endItem) {
+ if (endItem >= children.size()) {
+ endItem = children.size() - 1;
+ }
+ if (startItem >= Integer.MAX_VALUE) {
+ startItem = Integer.MAX_VALUE;
+ }
+ try {
+ List<MediaBrowser.MediaItem> childrenSubList =
+ children.subList((int) startItem, (int) endItem + 1);
+ if (childrenSubList.isEmpty()) {
+ Log.i(TAG, "childrenSubList is empty.");
+ throw new IndexOutOfBoundsException();
+ }
+ return childrenSubList;
+ } catch (IndexOutOfBoundsException ex) {
+ Log.w(TAG, "Index out of bounds start item =" + startItem + " end item = " + Math.min(
+ children.size(), endItem + 1));
+ return null;
+ } catch (IllegalArgumentException ex) {
+ Log.i(TAG, "Index out of bounds start item =" + startItem + " > size");
+ return null;
+ }
+ }
+
+
+ /*
+ * helper method to filter required attibutes before sending GetFolderItems response
+ */
+ public void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd mFolderItemsReqObj,
+ List<MediaBrowser.MediaItem> children, byte scope, long startItem, long endItem) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " + endItem);
+ }
+
+ List<MediaBrowser.MediaItem> resultItems = new ArrayList<MediaBrowser.MediaItem>();
+
+ if (children == null) {
+ Log.e(TAG, "Error: children are null in getFolderItemsFilterAttr");
+ mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
+ return;
+ }
+
+ /* check for index out of bound errors */
+ resultItems = checkIndexOutofBounds(bdaddr, children, startItem, endItem);
+ if (resultItems == null) {
+ Log.w(TAG, "resultItems is null.");
+ mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
+ return;
+ }
+ FolderItemsData folderDataNative = new FolderItemsData(resultItems.size());
+
+ /* variables to temperorily add attrs */
+ ArrayList<String> attrArray = new ArrayList<String>();
+ ArrayList<Integer> attrId = new ArrayList<Integer>();
+
+ for (int itemIndex = 0; itemIndex < resultItems.size(); itemIndex++) {
+ /* item type. Needs to be set by media player */
+ MediaBrowser.MediaItem item = resultItems.get(itemIndex);
+ int flags = item.getFlags();
+ if ((flags & MediaBrowser.MediaItem.FLAG_BROWSABLE) != 0) {
+ folderDataNative.mItemTypes[itemIndex] = AvrcpConstants.BTRC_ITEM_FOLDER;
+ } else {
+ folderDataNative.mItemTypes[itemIndex] = AvrcpConstants.BTRC_ITEM_MEDIA;
+ }
+
+ /* set playable */
+ if ((flags & MediaBrowser.MediaItem.FLAG_PLAYABLE) != 0) {
+ folderDataNative.mPlayable[itemIndex] = AvrcpConstants.ITEM_PLAYABLE;
+ } else {
+ folderDataNative.mPlayable[itemIndex] = AvrcpConstants.ITEM_NOT_PLAYABLE;
+ }
+ /* set uid for current item */
+ byte[] uid;
+ if (folderDataNative.mItemTypes[itemIndex] == AvrcpConstants.BTRC_ITEM_MEDIA)
+ uid = stringToByteMedia(item.getDescription().getMediaId());
+ else
+ uid = stringToByteFolder(item.getDescription().getMediaId());
+
+ for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
+ folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
+ }
+
+ /* Set display name for current item */
+ folderDataNative.mDisplayNames[itemIndex] =
+ getAttrValue(AvrcpConstants.ATTRID_TITLE, item);
+
+ int maxAttributesRequested = 0;
+ boolean isAllAttribRequested = false;
+ /* check if remote requested for attributes */
+ if (mFolderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
+ int attrCnt = 0;
+
+ /* add requested attr ids to a temp array */
+ if (mFolderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
+ isAllAttribRequested = true;
+ maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
+ } else {
+ /* get only the requested attribute ids from the request */
+ maxAttributesRequested = mFolderItemsReqObj.mNumAttr;
+ }
+
+ /* lookup and copy values of attributes for ids requested above */
+ for (int idx = 0; idx < maxAttributesRequested; idx++) {
+ /* check if media player provided requested attributes */
+ String value = null;
+
+ int attribId =
+ isAllAttribRequested ? (idx + 1) : mFolderItemsReqObj.mAttrIDs[idx];
+ value = getAttrValue(attribId, resultItems.get(itemIndex));
+ if (value != null) {
+ attrArray.add(value);
+ attrId.add(attribId);
+ attrCnt++;
+ }
+ }
+ /* add num attr actually received from media player for a particular item */
+ folderDataNative.mAttributesNum[itemIndex] = attrCnt;
+ }
+ }
+
+ /* copy filtered attr ids and attr values to response parameters */
+ if (attrId.size() > 0) {
+ folderDataNative.mAttrIds = new int[attrId.size()];
+ for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) {
+ folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
+ }
+ folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
+ }
+
+ /* create rsp object and send response to remote device */
+ FolderItemsRsp rspObj =
+ new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, scope,
+ folderDataNative.mNumItems, folderDataNative.mFolderTypes,
+ folderDataNative.mPlayable, folderDataNative.mItemTypes,
+ folderDataNative.mItemUid, folderDataNative.mDisplayNames,
+ folderDataNative.mAttributesNum, folderDataNative.mAttrIds,
+ folderDataNative.mAttrValues);
+ mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
+ }
+
+ public static String getAttrValue(int attr, MediaBrowser.MediaItem item) {
+ String attrValue = null;
+ try {
+ MediaDescription desc = item.getDescription();
+ Bundle extras = desc.getExtras();
+ switch (attr) {
+ /* Title is mandatory attribute */
+ case AvrcpConstants.ATTRID_TITLE:
+ attrValue = desc.getTitle().toString();
+ break;
+
+ case AvrcpConstants.ATTRID_ARTIST:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
+ break;
+
+ case AvrcpConstants.ATTRID_ALBUM:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
+ break;
+
+ case AvrcpConstants.ATTRID_TRACK_NUM:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
+ break;
+
+ case AvrcpConstants.ATTRID_NUM_TRACKS:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_NUM_TRACKS);
+ break;
+
+ case AvrcpConstants.ATTRID_GENRE:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
+ break;
+
+ case AvrcpConstants.ATTRID_PLAY_TIME:
+ attrValue = extras.getString(MediaMetadata.METADATA_KEY_DURATION);
+ break;
+
+ case AvrcpConstants.ATTRID_COVER_ART:
+ attrValue = Avrcp_ext.getImgHandleFromTitle(desc.getTitle().toString());
+ break;
+
+ default:
+ Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
+ return null;
+ }
+ } catch (NullPointerException ex) {
+ Log.w(TAG, "getAttrValue: attr id not found in result");
+ /* checking if attribute is title, then it is mandatory and cannot send null */
+ if (attr == AvrcpConstants.ATTRID_TITLE) {
+ attrValue = "<Unknown Title>";
+ } else {
+ return null;
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + " attr id: " + attr);
+ }
+ return attrValue;
+ }
+
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /* Helper methods */
+
+ /* check if item is browsable Down*/
+ private boolean isBrowsableFolderDn(String uid) {
+ for (MediaBrowser.MediaItem item : mFolderItems) {
+ if (item.getMediaId().equals(uid) && (
+ (item.getFlags() & MediaBrowser.MediaItem.FLAG_BROWSABLE)
+ == MediaBrowser.MediaItem.FLAG_BROWSABLE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /* check if browsable Up*/
+ private boolean isBrowsableFolderUp() {
+ if (mPathStack.peek().equals(mRootFolderUid)) {
+ /* Already on the root, cannot go up */
+ return false;
+ }
+ return true;
+ }
+
+ private String parseQueueId(String mediaId) {
+ if (isNumeric(mediaId)) {
+ Log.d(TAG, "Get queue id: " + mediaId);
+ return mediaId.trim();
+ } else {
+ String[] mediaIdItems = mediaId.split(",");
+ if (mediaIdItems != null && mediaIdItems.length > BROWSED_ITEM_ID_INDEX) {
+ Log.d(TAG, "Get queue id: " + mediaIdItems[BROWSED_ITEM_ID_INDEX]);
+ return mediaIdItems[BROWSED_ITEM_ID_INDEX].trim();
+ }
+ }
+
+ Log.d(TAG, "Unkown queue id");
+ return null;
+ }
+
+ /* convert uid to mediaId for Media item*/
+ private String byteToStringMedia(byte[] byteArray) {
+ int uid = new BigInteger(byteArray).intValue();
+ String mediaId = mMediaHmap.get(uid);
+ return mediaId;
+ }
+
+ /* convert uid to mediaId for Folder item*/
+ private String byteToStringFolder(byte[] byteArray) {
+ int uid = new BigInteger(byteArray).intValue();
+ String mediaId = mFolderHmap.get(uid);
+ return mediaId;
+ }
+
+ /* convert mediaId to uid for Media item*/
+ private byte[] stringToByteMedia(String mediaId) {
+ /* check if this mediaId already exists in hashmap */
+ if (!mMediaHmap.containsValue(mediaId)) { /* add to hashmap */
+ int uid;
+ String queueId = parseQueueId(mediaId);
+ if (queueId == null) {
+ uid = mMediaHmap.size() + 1;
+ } else {
+ uid = Integer.valueOf(queueId).intValue();
+ }
+
+ mMediaHmap.put(uid, mediaId);
+ return intToByteArray(uid);
+ } else { /* search key for give mediaId */
+ for (int uid : mMediaHmap.keySet()) {
+ if (mMediaHmap.get(uid).equals(mediaId)) {
+ return intToByteArray(uid);
+ }
+ }
+ }
+ return null;
+ }
+
+ /* convert mediaId to uid for Folder item*/
+ private byte[] stringToByteFolder(String mediaId) {
+ /* check if this mediaId already exists in hashmap */
+ if (!mFolderHmap.containsValue(mediaId)) { /* add to hashmap */
+ // Offset by one as uid 0 is reserved
+ int uid = mFolderHmap.size() + 1;
+ mFolderHmap.put(uid, mediaId);
+ return intToByteArray(uid);
+ } else { /* search key for give mediaId */
+ for (int uid : mFolderHmap.keySet()) {
+ if (mFolderHmap.get(uid).equals(mediaId)) {
+ return intToByteArray(uid);
+ }
+ }
+ }
+ return null;
+ }
+
+ private void refreshFolderItems(List<MediaBrowser.MediaItem> folderItems) {
+ for (int itemIndex = 0; itemIndex < folderItems.size(); itemIndex++) {
+ MediaBrowser.MediaItem item = folderItems.get(itemIndex);
+ int flags = item.getFlags();
+ if ((flags & MediaBrowser.MediaItem.FLAG_BROWSABLE) != 0) {
+ Log.d(TAG, "Folder item, no need refresh hashmap from mediaId to uid");
+ } else {
+ Log.d(TAG, "Media item, refresh haspmap from mediaId to uid");
+ stringToByteMedia(item.getDescription().getMediaId());
+ }
+ }
+ }
+
+ /* converts queue item received from getQueue call, to MediaItem used by FilterAttr method */
+ private List<MediaBrowser.MediaItem> queueItem2MediaItem(
+ List<MediaSession.QueueItem> tempItems) {
+
+ List<MediaBrowser.MediaItem> tempMedia = new ArrayList<MediaBrowser.MediaItem>();
+ for (int itemCount = 0; itemCount < tempItems.size(); itemCount++) {
+ MediaDescription.Builder build = new MediaDescription.Builder();
+ build.setMediaId(Long.toString(tempItems.get(itemCount).getQueueId()));
+ build.setTitle(tempItems.get(itemCount).getDescription().getTitle());
+ build.setExtras(tempItems.get(itemCount).getDescription().getExtras());
+ MediaDescription des = build.build();
+ MediaItem item = new MediaItem((des), MediaItem.FLAG_PLAYABLE);
+ tempMedia.add(item);
+ }
+ return tempMedia;
+ }
+
+ /* convert integer to byte array of size 8 bytes */
+ public byte[] intToByteArray(int value) {
+ int index = 0;
+ byte[] encodedValue = new byte[AvrcpConstants.UID_SIZE];
+
+ encodedValue[index++] = (byte) 0x00;
+ encodedValue[index++] = (byte) 0x00;
+ encodedValue[index++] = (byte) 0x00;
+ encodedValue[index++] = (byte) 0x00;
+ encodedValue[index++] = (byte) (value >> 24);
+ encodedValue[index++] = (byte) (value >> 16);
+ encodedValue[index++] = (byte) (value >> 8);
+ encodedValue[index++] = (byte) value;
+
+ return encodedValue;
+ }
+
+ public boolean isNumeric(String mediaId) {
+ String trimStr = mediaId.trim();
+ int length = trimStr.length();
+
+ for(int i = 0; i < length; i++) {
+ char c = trimStr.charAt(i);
+ if (!((c >= '0' && c <= '9'))) {
+ Log.v(TAG, "Non-Numeric media Id");
+ return false;
+ }
+ }
+ Log.v(TAG, "Numeric media Id");
+ return true;
+ }
+}
diff --git a/packages_apps_bluetooth_ext/com_android_bluetooth_avrcp.cpp b/packages_apps_bluetooth_ext/com_android_bluetooth_avrcp.cpp
new file mode 100644
index 0000000..fc72517
--- /dev/null
+++ b/packages_apps_bluetooth_ext/com_android_bluetooth_avrcp.cpp
@@ -0,0 +1,1554 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "BluetoothAvrcpServiceJni"
+
+#define LOG_NDEBUG 0
+
+#include "android_runtime/AndroidRuntime.h"
+#include "com_android_bluetooth.h"
+#include "hardware/bt_rc.h"
+#include "utils/Log.h"
+
+#include <inttypes.h>
+#include <string.h>
+#include <mutex>
+#include <shared_mutex>
+
+namespace android {
+static jmethodID method_getRcFeatures;
+static jmethodID method_getPlayStatus;
+static jmethodID method_getElementAttr;
+static jmethodID method_registerNotification;
+static jmethodID method_volumeChangeCallback;
+static jmethodID method_handlePassthroughCmd;
+static jmethodID method_getFolderItemsCallback;
+static jmethodID method_setAddressedPlayerCallback;
+
+static jmethodID method_setBrowsedPlayerCallback;
+static jmethodID method_changePathCallback;
+static jmethodID method_searchCallback;
+static jmethodID method_playItemCallback;
+static jmethodID method_getItemAttrCallback;
+static jmethodID method_addToPlayListCallback;
+static jmethodID method_getTotalNumOfItemsCallback;
+
+static const btrc_interface_t* sBluetoothAvrcpInterface = NULL;
+static jobject mCallbacksObj = NULL;
+static std::shared_timed_mutex callbacks_mutex;
+
+/* Function declarations */
+static bool copy_item_attributes(JNIEnv* env, jobject object,
+ btrc_folder_items_t* pitem,
+ jint* p_attributesIds,
+ jobjectArray attributesArray, int item_idx,
+ int attribCopiedIndex);
+
+static bool copy_jstring(uint8_t* str, int maxBytes, jstring jstr, JNIEnv* env);
+
+static void cleanup_items(btrc_folder_items_t* p_items, int numItems);
+
+static void btavrcp_remote_features_callback(const RawAddress& bd_addr,
+ btrc_remote_features_t features) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Unable to allocate byte array for bd_addr");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getRcFeatures, addr.get(),
+ (jint)features);
+}
+
+/** Callback for play status request */
+static void btavrcp_get_play_status_callback(const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for get_play_status command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getPlayStatus, addr.get());
+}
+
+static void btavrcp_get_element_attr_callback(uint8_t num_attr,
+ btrc_media_attr_t* p_attrs,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for get_element_attr command");
+ return;
+ }
+
+ ScopedLocalRef<jintArray> attrs(
+ sCallbackEnv.get(), (jintArray)sCallbackEnv->NewIntArray(num_attr));
+ if (!attrs.get()) {
+ ALOGE("Fail to new jintArray for attrs");
+ return;
+ }
+
+ sCallbackEnv->SetIntArrayRegion(attrs.get(), 0, num_attr, (jint*)p_attrs);
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getElementAttr, addr.get(),
+ (jbyte)num_attr, attrs.get());
+}
+
+static void btavrcp_register_notification_callback(btrc_event_id_t event_id,
+ uint32_t param,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for register_notification command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_registerNotification,
+ addr.get(), (jint)event_id, (jint)param);
+}
+
+static void btavrcp_volume_change_callback(uint8_t volume, uint8_t ctype,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for volume_change command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_volumeChangeCallback,
+ addr.get(), (jint)volume, (jint)ctype);
+}
+
+static void btavrcp_passthrough_command_callback(int id, int pressed,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for passthrough_command command");
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handlePassthroughCmd,
+ addr.get(), (jint)id, (jint)pressed);
+}
+
+static void btavrcp_set_addressed_player_callback(uint16_t player_id,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for set_addressed_player command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_setAddressedPlayerCallback,
+ addr.get(), (jint)player_id);
+}
+
+static void btavrcp_set_browsed_player_callback(uint16_t player_id,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for set_browsed_player command");
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_setBrowsedPlayerCallback,
+ addr.get(), (jint)player_id);
+}
+
+static void btavrcp_get_folder_items_callback(
+ uint8_t scope, uint32_t start_item, uint32_t end_item, uint8_t num_attr,
+ uint32_t* p_attr_ids, const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for get_folder_items command");
+ return;
+ }
+
+ uint32_t* puiAttr = (uint32_t*)p_attr_ids;
+ ScopedLocalRef<jintArray> attr_ids(sCallbackEnv.get(), NULL);
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+
+ /* check number of attributes requested by remote device */
+ if ((num_attr != BTRC_NUM_ATTR_ALL) && (num_attr != BTRC_NUM_ATTR_NONE)) {
+ /* allocate memory for attr_ids only if some attributes passed from below
+ * layer */
+ attr_ids.reset((jintArray)sCallbackEnv->NewIntArray(num_attr));
+ if (!attr_ids.get()) {
+ ALOGE("Fail to allocate new jintArray for attrs");
+ return;
+ }
+ sCallbackEnv->SetIntArrayRegion(attr_ids.get(), 0, num_attr,
+ (jint*)puiAttr);
+ }
+
+ sCallbackEnv->CallVoidMethod(
+ mCallbacksObj, method_getFolderItemsCallback, addr.get(), (jbyte)scope,
+ (jlong)start_item, (jlong)end_item, (jbyte)num_attr, attr_ids.get());
+}
+
+static void btavrcp_change_path_callback(uint8_t direction, uint8_t* folder_uid,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
+ sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
+ if (!attrs.get()) {
+ ALOGE("Fail to new jintArray for attrs");
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for change_path command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->SetByteArrayRegion(
+ attrs.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)folder_uid);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_changePathCallback,
+ addr.get(), (jbyte)direction, attrs.get());
+}
+
+static void btavrcp_get_item_attr_callback(uint8_t scope, uint8_t* uid,
+ uint16_t uid_counter,
+ uint8_t num_attr,
+ btrc_media_attr_t* p_attrs,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> attr_uid(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
+ if (!attr_uid.get()) {
+ ALOGE("Fail to new jintArray for attr_uid");
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for get_item_attr command");
+ return;
+ }
+
+ ScopedLocalRef<jintArray> attrs(
+ sCallbackEnv.get(), (jintArray)sCallbackEnv->NewIntArray(num_attr));
+ if (!attrs.get()) {
+ ALOGE("Fail to new jintArray for attrs");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->SetIntArrayRegion(attrs.get(), 0, num_attr, (jint*)p_attrs);
+ sCallbackEnv->SetByteArrayRegion(
+ attr_uid.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)uid);
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getItemAttrCallback,
+ addr.get(), (jbyte)scope, attr_uid.get(),
+ (jint)uid_counter, (jbyte)num_attr, attrs.get());
+}
+
+static void btavrcp_play_item_callback(uint8_t scope, uint16_t uid_counter,
+ uint8_t* uid,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
+ sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
+ if (!attrs.get()) {
+ ALOGE("%s: Fail to new jByteArray attrs for play_item command", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for play_item command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->SetByteArrayRegion(
+ attrs.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)uid);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_playItemCallback,
+ addr.get(), (jbyte)scope, (jint)uid_counter,
+ attrs.get());
+}
+
+static void btavrcp_get_total_num_items_callback(uint8_t scope,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for get_total_num_items command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getTotalNumOfItemsCallback,
+ addr.get(), (jbyte)scope);
+}
+
+static void btavrcp_search_callback(uint16_t charset_id, uint16_t str_len,
+ uint8_t* p_str, const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
+ sCallbackEnv->NewByteArray(str_len));
+ if (!attrs.get()) {
+ ALOGE("Fail to new jintArray for attrs");
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for search command");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->SetByteArrayRegion(attrs.get(), 0, str_len * sizeof(uint8_t),
+ (jbyte*)p_str);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_searchCallback, addr.get(),
+ (jint)charset_id, attrs.get());
+}
+
+static void btavrcp_add_to_play_list_callback(uint8_t scope, uint8_t* uid,
+ uint16_t uid_counter,
+ const RawAddress& bd_addr) {
+ CallbackEnv sCallbackEnv(__func__);
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ if (!sCallbackEnv.valid()) return;
+ if (!mCallbacksObj) {
+ ALOGE("%s: mCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("Fail to new jbyteArray bd addr for add_to_play_list command");
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
+ sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
+ if (!attrs.get()) {
+ ALOGE("Fail to new jByteArray for attrs");
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)bd_addr.address);
+ sCallbackEnv->SetByteArrayRegion(
+ attrs.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)uid);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_addToPlayListCallback,
+ addr.get(), (jbyte)scope, attrs.get(),
+ (jint)uid_counter);
+}
+
+static btrc_callbacks_t sBluetoothAvrcpCallbacks = {
+ sizeof(sBluetoothAvrcpCallbacks),
+ btavrcp_remote_features_callback,
+ btavrcp_get_play_status_callback,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ btavrcp_get_element_attr_callback,
+ btavrcp_register_notification_callback,
+ btavrcp_volume_change_callback,
+ btavrcp_passthrough_command_callback,
+ btavrcp_set_addressed_player_callback,
+ btavrcp_set_browsed_player_callback,
+ btavrcp_get_folder_items_callback,
+ btavrcp_change_path_callback,
+ btavrcp_get_item_attr_callback,
+ btavrcp_play_item_callback,
+ btavrcp_get_total_num_items_callback,
+ btavrcp_search_callback,
+ btavrcp_add_to_play_list_callback,
+};
+
+static void classInitNative(JNIEnv* env, jclass clazz) {
+ method_getRcFeatures =
+ env->GetMethodID(clazz, "getRcFeaturesRequestFromNative", "([BI)V");
+ method_getPlayStatus =
+ env->GetMethodID(clazz, "getPlayStatusRequestFromNative", "([B)V");
+
+ method_getElementAttr =
+ env->GetMethodID(clazz, "getElementAttrRequestFromNative", "([BB[I)V");
+
+ method_registerNotification = env->GetMethodID(
+ clazz, "registerNotificationRequestFromNative", "([BII)V");
+
+ method_volumeChangeCallback =
+ env->GetMethodID(clazz, "volumeChangeRequestFromNative", "([BII)V");
+
+ method_handlePassthroughCmd = env->GetMethodID(
+ clazz, "handlePassthroughCmdRequestFromNative", "([BII)V");
+
+ method_setAddressedPlayerCallback =
+ env->GetMethodID(clazz, "setAddressedPlayerRequestFromNative", "([BI)V");
+
+ method_setBrowsedPlayerCallback =
+ env->GetMethodID(clazz, "setBrowsedPlayerRequestFromNative", "([BI)V");
+
+ method_getFolderItemsCallback =
+ env->GetMethodID(clazz, "getFolderItemsRequestFromNative", "([BBJJB[I)V");
+
+ method_changePathCallback =
+ env->GetMethodID(clazz, "changePathRequestFromNative", "([BB[B)V");
+
+ method_getItemAttrCallback =
+ env->GetMethodID(clazz, "getItemAttrRequestFromNative", "([BB[BIB[I)V");
+
+ method_playItemCallback =
+ env->GetMethodID(clazz, "playItemRequestFromNative", "([BBI[B)V");
+
+ method_getTotalNumOfItemsCallback =
+ env->GetMethodID(clazz, "getTotalNumOfItemsRequestFromNative", "([BB)V");
+
+ method_searchCallback =
+ env->GetMethodID(clazz, "searchRequestFromNative", "([BI[B)V");
+
+ method_addToPlayListCallback =
+ env->GetMethodID(clazz, "addToPlayListRequestFromNative", "([BB[BI)V");
+
+ ALOGI("%s: succeeds", __func__);
+}
+
+static void initNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ const bt_interface_t* btInf = getBluetoothInterface();
+ if (btInf == NULL) {
+ ALOGE("Bluetooth module is not loaded");
+ return;
+ }
+
+ if (sBluetoothAvrcpInterface != NULL) {
+ ALOGW("Cleaning up Avrcp Interface before initializing...");
+ sBluetoothAvrcpInterface->cleanup();
+ sBluetoothAvrcpInterface = NULL;
+ }
+
+ if (mCallbacksObj != NULL) {
+ ALOGW("Cleaning up Avrcp callback object");
+ env->DeleteGlobalRef(mCallbacksObj);
+ mCallbacksObj = NULL;
+ }
+
+ sBluetoothAvrcpInterface =
+ (btrc_interface_t*)btInf->get_profile_interface(BT_PROFILE_AV_RC_ID);
+ if (sBluetoothAvrcpInterface == NULL) {
+ ALOGE("Failed to get Bluetooth Avrcp Interface");
+ return;
+ }
+
+ bt_status_t status =
+ sBluetoothAvrcpInterface->init(&sBluetoothAvrcpCallbacks);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed to initialize Bluetooth Avrcp, status: %d", status);
+ sBluetoothAvrcpInterface = NULL;
+ return;
+ }
+
+ mCallbacksObj = env->NewGlobalRef(object);
+}
+
+static void cleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ const bt_interface_t* btInf = getBluetoothInterface();
+ if (btInf == NULL) {
+ ALOGE("Bluetooth module is not loaded");
+ return;
+ }
+
+ if (sBluetoothAvrcpInterface != NULL) {
+ sBluetoothAvrcpInterface->cleanup();
+ sBluetoothAvrcpInterface = NULL;
+ }
+
+ if (mCallbacksObj != NULL) {
+ env->DeleteGlobalRef(mCallbacksObj);
+ mCallbacksObj = NULL;
+ }
+}
+
+static jboolean getPlayStatusRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint playStatus,
+ jint songLen, jint songPos) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+ RawAddress rawAddress;
+ rawAddress.FromOctets((uint8_t*)addr);
+
+ bt_status_t status = sBluetoothAvrcpInterface->get_play_status_rsp(
+ rawAddress, (btrc_play_status_t)playStatus, songLen, songPos);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed get_play_status_rsp, status: %d", status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean getElementAttrRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jbyte numAttr,
+ jintArray attrIds,
+ jobjectArray textArray) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ if (numAttr > BTRC_MAX_ELEM_ATTR_SIZE) {
+ ALOGE("get_element_attr_rsp: number of attributes exceed maximum");
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ btrc_element_attr_val_t* pAttrs = new btrc_element_attr_val_t[numAttr];
+ if (!pAttrs) {
+ ALOGE("get_element_attr_rsp: not have enough memeory");
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_FALSE;
+ }
+
+ jint* attr = env->GetIntArrayElements(attrIds, NULL);
+ if (!attr) {
+ delete[] pAttrs;
+ jniThrowIOException(env, EINVAL);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_FALSE;
+ }
+
+ int attr_cnt;
+ for (attr_cnt = 0; attr_cnt < numAttr; ++attr_cnt) {
+ pAttrs[attr_cnt].attr_id = attr[attr_cnt];
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(textArray, attr_cnt));
+
+ if (!copy_jstring(pAttrs[attr_cnt].text, BTRC_MAX_ATTR_STR_LEN, text.get(),
+ env)) {
+ break;
+ }
+ }
+
+ if (attr_cnt < numAttr) {
+ delete[] pAttrs;
+ env->ReleaseIntArrayElements(attrIds, attr, 0);
+ ALOGE("%s: Failed to copy attributes", __func__);
+ return JNI_FALSE;
+ }
+
+ RawAddress rawAddress;
+ rawAddress.FromOctets((uint8_t*)addr);
+ bt_status_t status = sBluetoothAvrcpInterface->get_element_attr_rsp(
+ rawAddress, numAttr, pAttrs);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed get_element_attr_rsp, status: %d", status);
+ }
+
+ delete[] pAttrs;
+ env->ReleaseIntArrayElements(attrIds, attr, 0);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean getItemAttrRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint rspStatus,
+ jbyte numAttr, jintArray attrIds,
+ jobjectArray textArray) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ if (numAttr > BTRC_MAX_ELEM_ATTR_SIZE) {
+ ALOGE("get_element_attr_rsp: number of attributes exceed maximum");
+ return JNI_FALSE;
+ }
+
+ btrc_element_attr_val_t* pAttrs = new btrc_element_attr_val_t[numAttr];
+ if (!pAttrs) {
+ ALOGE("%s: not have enough memory", __func__);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_FALSE;
+ }
+
+ jint* attr = NULL;
+ if (attrIds != NULL) {
+ attr = env->GetIntArrayElements(attrIds, NULL);
+ if (!attr) {
+ delete[] pAttrs;
+ jniThrowIOException(env, EINVAL);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_FALSE;
+ }
+
+ for (int attr_cnt = 0; attr_cnt < numAttr; ++attr_cnt) {
+ pAttrs[attr_cnt].attr_id = attr[attr_cnt];
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(textArray, attr_cnt));
+
+ if (!copy_jstring(pAttrs[attr_cnt].text, BTRC_MAX_ATTR_STR_LEN, text.get(),
+ env)) {
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ ALOGE("%s: Failed to copy attributes", __func__);
+ break;
+ }
+ }
+ }
+ RawAddress rawAddress;
+ rawAddress.FromOctets((uint8_t*)addr);
+
+ bt_status_t status = sBluetoothAvrcpInterface->get_item_attr_rsp(
+ rawAddress, (btrc_status_t)rspStatus, numAttr, pAttrs);
+ if (status != BT_STATUS_SUCCESS)
+ ALOGE("Failed get_item_attr_rsp, status: %d", status);
+
+ if (pAttrs) delete[] pAttrs;
+ if (attr) env->ReleaseIntArrayElements(attrIds, attr, 0);
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspPlayStatusNative(JNIEnv* env,
+ jobject object,
+ jint type,
+ jint playStatus) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ param.play_status = (btrc_play_status_t)playStatus;
+
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_PLAY_STATUS_CHANGED, (btrc_notification_type_t)type, ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed register_notification_rsp play status, status: %d", status);
+ }
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspTrackChangeNative(JNIEnv* env,
+ jobject object,
+ jint type,
+ jbyteArray track) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* trk = env->GetByteArrayElements(track, NULL);
+ if (!trk) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ uint64_t uid = 0;
+ for (int uid_idx = 0; uid_idx < BTRC_UID_SIZE; ++uid_idx) {
+ param.track[uid_idx] = trk[uid_idx];
+ uid = uid + (trk[uid_idx] << (BTRC_UID_SIZE - 1 - uid_idx));
+ }
+
+ ALOGV("%s: Sending track change notification: %d -> %" PRIu64, __func__, type,
+ uid);
+
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_TRACK_CHANGE, (btrc_notification_type_t)type, ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed register_notification_rsp track change, status: %d", status);
+ }
+
+ env->ReleaseByteArrayElements(track, trk, 0);
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspPlayPosNative(JNIEnv* env,
+ jobject object, jint type,
+ jint playPos) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ param.song_pos = (uint32_t)playPos;
+
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_PLAY_POS_CHANGED, (btrc_notification_type_t)type, ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed register_notification_rsp play position, status: %d", status);
+ }
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspNowPlayingChangedNative(JNIEnv* env,
+ jobject object,
+ jint type) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_NOW_PLAYING_CONTENT_CHANGED, (btrc_notification_type_t)type,
+ ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed register_notification_rsp, nowPlaying Content status: %d",
+ status);
+ }
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspUIDsChangedNative(JNIEnv* env,
+ jobject object,
+ jint type,
+ jint uidCounter) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ param.uids_changed.uid_counter = (uint16_t)uidCounter;
+
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_UIDS_CHANGED, (btrc_notification_type_t)type, ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed register_notification_rsp, uids changed status: %d", status);
+ }
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspAddrPlayerChangedNative(
+ JNIEnv* env, jobject object, jint type, jint playerId, jint uidCounter) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ param.addr_player_changed.player_id = (uint16_t)playerId;
+ param.addr_player_changed.uid_counter = (uint16_t)uidCounter;
+
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_ADDR_PLAYER_CHANGE, (btrc_notification_type_t)type, ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed register_notification_rsp address player changed status: %d",
+ status);
+ }
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean registerNotificationRspAvalPlayerChangedNative(JNIEnv* env,
+ jobject object,
+ jint type) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ btrc_register_notification_t param;
+ bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
+ BTRC_EVT_AVAL_PLAYER_CHANGE, (btrc_notification_type_t)type, ¶m);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE(
+ "Failed register_notification_rsp available player changed status, "
+ "status: %d",
+ status);
+ }
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean setVolumeNative(JNIEnv* env, jobject object, jint volume) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ bt_status_t status = sBluetoothAvrcpInterface->set_volume((uint8_t)volume);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed set_volume, status: %d", status);
+ }
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+/* native response for scope as Media player */
+static jboolean mediaPlayerListRspNative(
+ JNIEnv* env, jobject object, jbyteArray address, jint rspStatus,
+ jint uidCounter, jbyte itemType, jint numItems, jintArray playerIds,
+ jbyteArray playerTypes, jintArray playerSubtypes,
+ jbyteArray playStatusValues, jshortArray featureBitmask,
+ jobjectArray textArray) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ jbyte *p_playerTypes = NULL, *p_PlayStatusValues = NULL;
+ jshort* p_FeatBitMaskValues = NULL;
+ jint *p_playerIds = NULL, *p_playerSubTypes = NULL;
+ btrc_folder_items_t* p_items = NULL;
+ if (rspStatus == BTRC_STS_NO_ERROR) {
+ /* allocate memory */
+ p_playerIds = env->GetIntArrayElements(playerIds, NULL);
+ p_playerTypes = env->GetByteArrayElements(playerTypes, NULL);
+ p_playerSubTypes = env->GetIntArrayElements(playerSubtypes, NULL);
+ p_PlayStatusValues = env->GetByteArrayElements(playStatusValues, NULL);
+ p_FeatBitMaskValues = env->GetShortArrayElements(featureBitmask, NULL);
+ p_items = new btrc_folder_items_t[numItems];
+ /* deallocate memory and return if allocation failed */
+ if (!p_playerIds || !p_playerTypes || !p_playerSubTypes ||
+ !p_PlayStatusValues || !p_FeatBitMaskValues || !p_items) {
+ if (p_playerIds) env->ReleaseIntArrayElements(playerIds, p_playerIds, 0);
+ if (p_playerTypes)
+ env->ReleaseByteArrayElements(playerTypes, p_playerTypes, 0);
+ if (p_playerSubTypes)
+ env->ReleaseIntArrayElements(playerSubtypes, p_playerSubTypes, 0);
+ if (p_PlayStatusValues)
+ env->ReleaseByteArrayElements(playStatusValues, p_PlayStatusValues, 0);
+ if (p_FeatBitMaskValues)
+ env->ReleaseShortArrayElements(featureBitmask, p_FeatBitMaskValues, 0);
+ if (p_items) delete[] p_items;
+
+ jniThrowIOException(env, EINVAL);
+ ALOGE("%s: not have enough memory", __func__);
+ return JNI_FALSE;
+ }
+
+ p_items->item_type = (uint8_t)itemType;
+
+ /* copy list of media players along with other parameters */
+ int itemIdx;
+ for (itemIdx = 0; itemIdx < numItems; ++itemIdx) {
+ p_items[itemIdx].player.player_id = p_playerIds[itemIdx];
+ p_items[itemIdx].player.major_type = p_playerTypes[itemIdx];
+ p_items[itemIdx].player.sub_type = p_playerSubTypes[itemIdx];
+ p_items[itemIdx].player.play_status = p_PlayStatusValues[itemIdx];
+ p_items[itemIdx].player.charset_id = BTRC_CHARSET_ID_UTF8;
+
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(textArray, itemIdx));
+ /* copy player name */
+ if (!copy_jstring(p_items[itemIdx].player.name, BTRC_MAX_ATTR_STR_LEN,
+ text.get(), env))
+ break;
+
+ /* Feature bit mask is 128-bit value each */
+ for (int InnCnt = 0; InnCnt < 16; InnCnt++) {
+ p_items[itemIdx].player.features[InnCnt] =
+ (uint8_t)p_FeatBitMaskValues[(itemIdx * 16) + InnCnt];
+ }
+ }
+
+ /* failed to copy list of media players */
+ if (itemIdx < numItems) {
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ ALOGE("%s: Failed to copy Media player attributes", __func__);
+ }
+ }
+
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->get_folder_items_list_rsp(
+ *btAddr, (btrc_status_t)rspStatus, uidCounter, numItems, p_items);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed get_folder_items_list_rsp, status: %d", status);
+ }
+
+ /* release allocated memory */
+ if (p_items) delete[] p_items;
+ if (p_playerTypes)
+ env->ReleaseByteArrayElements(playerTypes, p_playerTypes, 0);
+ if (p_playerSubTypes)
+ env->ReleaseIntArrayElements(playerSubtypes, p_playerSubTypes, 0);
+ if (p_PlayStatusValues)
+ env->ReleaseByteArrayElements(playStatusValues, p_PlayStatusValues, 0);
+ if (p_FeatBitMaskValues) {
+ env->ReleaseShortArrayElements(featureBitmask, p_FeatBitMaskValues, 0);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean getFolderItemsRspNative(
+ JNIEnv* env, jobject object, jbyteArray address, jint rspStatus,
+ jshort uidCounter, jbyte scope, jint numItems, jbyteArray folderType,
+ jbyteArray playable, jbyteArray itemType, jbyteArray itemUidArray,
+ jobjectArray displayNameArray, jintArray numAttrs, jintArray attributesIds,
+ jobjectArray attributesArray) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ jbyte *p_playable = NULL, *p_item_uid = NULL;
+ jbyte* p_item_types = NULL; /* Folder or Media Item */
+ jint* p_attributesIds = NULL;
+ jbyte* p_folder_types =
+ NULL; /* Folder properties like Album/Genre/Artists etc */
+ jint* p_num_attrs = NULL;
+ btrc_folder_items_t* p_items = NULL;
+ /* none of the parameters should be null when no error */
+ if (rspStatus == BTRC_STS_NO_ERROR) {
+ /* allocate memory to each rsp item */
+ if (folderType != NULL)
+ p_folder_types = env->GetByteArrayElements(folderType, NULL);
+ if (playable != NULL)
+ p_playable = env->GetByteArrayElements(playable, NULL);
+ if (itemType != NULL)
+ p_item_types = env->GetByteArrayElements(itemType, NULL);
+ if (NULL != numAttrs)
+ p_num_attrs = env->GetIntArrayElements(numAttrs, NULL);
+ if (NULL != attributesIds)
+ p_attributesIds = env->GetIntArrayElements(attributesIds, NULL);
+ if (itemUidArray != NULL)
+ p_item_uid = (jbyte*)env->GetByteArrayElements(itemUidArray, NULL);
+
+ p_items = new btrc_folder_items_t[numItems];
+
+ /* if memory alloc failed, release memory */
+ if (p_items && p_folder_types && p_playable && p_item_types && p_item_uid &&
+ /* attributes can be null if remote requests 0 attributes */
+ ((numAttrs != NULL && p_num_attrs) || (!numAttrs && !p_num_attrs)) &&
+ ((attributesIds != NULL && p_attributesIds) ||
+ (!attributesIds && !p_attributesIds))) {
+ memset(p_items, 0, sizeof(btrc_folder_items_t) * numItems);
+ if (scope == BTRC_SCOPE_FILE_SYSTEM || scope == BTRC_SCOPE_SEARCH ||
+ scope == BTRC_SCOPE_NOW_PLAYING) {
+ int attribCopiedIndex = 0;
+ for (int item_idx = 0; item_idx < numItems; item_idx++) {
+ if (BTRC_ITEM_FOLDER == p_item_types[item_idx]) {
+ btrc_folder_items_t* pitem = &p_items[item_idx];
+
+ memcpy(pitem->folder.uid, p_item_uid + item_idx * BTRC_UID_SIZE,
+ BTRC_UID_SIZE);
+ pitem->item_type = (uint8_t)BTRC_ITEM_FOLDER;
+ pitem->folder.charset_id = BTRC_CHARSET_ID_UTF8;
+ pitem->folder.type = p_folder_types[item_idx];
+ pitem->folder.playable = p_playable[item_idx];
+
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(displayNameArray,
+ item_idx));
+ if (!copy_jstring(pitem->folder.name, BTRC_MAX_ATTR_STR_LEN,
+ text.get(), env)) {
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ ALOGE("%s: failed to copy display name of folder item", __func__);
+ break;
+ }
+ } else if (BTRC_ITEM_MEDIA == p_item_types[item_idx]) {
+ btrc_folder_items_t* pitem = &p_items[item_idx];
+ memcpy(pitem->media.uid, p_item_uid + item_idx * BTRC_UID_SIZE,
+ BTRC_UID_SIZE);
+
+ pitem->item_type = (uint8_t)BTRC_ITEM_MEDIA;
+ pitem->media.charset_id = BTRC_CHARSET_ID_UTF8;
+ pitem->media.type = BTRC_MEDIA_TYPE_AUDIO;
+ pitem->media.num_attrs =
+ (p_num_attrs != NULL) ? p_num_attrs[item_idx] : 0;
+
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(displayNameArray,
+ item_idx));
+ if (!copy_jstring(pitem->media.name, BTRC_MAX_ATTR_STR_LEN,
+ text.get(), env)) {
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ ALOGE("%s: failed to copy display name of media item", __func__);
+ break;
+ }
+
+ /* copy item attributes */
+ if (!copy_item_attributes(env, object, pitem, p_attributesIds,
+ attributesArray, item_idx,
+ attribCopiedIndex)) {
+ ALOGE("%s: error in copying attributes of item = %s", __func__,
+ pitem->media.name);
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ break;
+ }
+ attribCopiedIndex += pitem->media.num_attrs;
+ }
+ }
+ }
+ } else {
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ ALOGE("%s: unable to allocate memory", __func__);
+ }
+ }
+
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->get_folder_items_list_rsp(
+ *btAddr, (btrc_status_t)rspStatus, uidCounter, numItems, p_items);
+ if (status != BT_STATUS_SUCCESS)
+ ALOGE("Failed get_folder_items_list_rsp, status: %d", status);
+
+ /* Release allocated memory for all attributes in each media item */
+ if (p_items) cleanup_items(p_items, numItems);
+
+ /* Release allocated memory */
+ if (p_folder_types)
+ env->ReleaseByteArrayElements(folderType, p_folder_types, 0);
+ if (p_playable) env->ReleaseByteArrayElements(playable, p_playable, 0);
+ if (p_item_types) env->ReleaseByteArrayElements(itemType, p_item_types, 0);
+ if (p_num_attrs) env->ReleaseIntArrayElements(numAttrs, p_num_attrs, 0);
+ if (p_attributesIds)
+ env->ReleaseIntArrayElements(attributesIds, p_attributesIds, 0);
+ if (p_item_uid) env->ReleaseByteArrayElements(itemUidArray, p_item_uid, 0);
+ if (p_items) delete[] p_items;
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean setAddressedPlayerRspNative(JNIEnv* env, jobject object,
+ jbyteArray address,
+ jint rspStatus) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->set_addressed_player_rsp(
+ *btAddr, (btrc_status_t)rspStatus);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed set_addressed_player_rsp, status: %d", status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean setBrowsedPlayerRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint rspStatus,
+ jbyte depth, jint numItems,
+ jobjectArray textArray) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ btrc_br_folder_name_t* p_folders = NULL;
+ if (rspStatus == BTRC_STS_NO_ERROR) {
+ if (depth > 0) {
+ p_folders = new btrc_br_folder_name_t[depth];
+
+ if (!p_folders ) {
+ jniThrowIOException(env, EINVAL);
+ ALOGE("%s: not have enough memeory", __func__);
+ return JNI_FALSE;
+ }
+ for (int folder_idx = 0; folder_idx < depth; folder_idx++) {
+ /* copy folder names */
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(textArray, folder_idx));
+
+ if (!copy_jstring(p_folders[folder_idx].p_str, BTRC_MAX_ATTR_STR_LEN,
+ text.get(), env)) {
+ rspStatus = BTRC_STS_INTERNAL_ERR;
+ delete[] p_folders;
+ env->ReleaseByteArrayElements(address, addr, 0);
+ ALOGE("%s: Failed to copy folder name", __func__);
+ return JNI_FALSE;
+ }
+
+ p_folders[folder_idx].str_len =
+ strlen((char*)p_folders[folder_idx].p_str);
+ }
+ }
+ }
+
+ uint8_t folder_depth =
+ depth; /* folder_depth is 0 if current folder is root */
+ uint16_t charset_id = BTRC_CHARSET_ID_UTF8;
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->set_browsed_player_rsp(
+ *btAddr, (btrc_status_t)rspStatus, numItems, charset_id, folder_depth,
+ p_folders);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("%s: Failed set_browsed_player_rsp, status: %d", __func__, status);
+ }
+
+ if (depth > 0) {
+ delete[] p_folders;
+ }
+
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean changePathRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint rspStatus,
+ jint numItems) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ uint32_t nItems = (uint32_t)numItems;
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->change_path_rsp(
+ *btAddr, (btrc_status_t)rspStatus, (uint32_t)nItems);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed change_path_rsp, status: %d", status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean searchRspNative(JNIEnv* env, jobject object, jbyteArray address,
+ jint rspStatus, jint uidCounter,
+ jint numItems) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ uint32_t nItems = (uint32_t)numItems;
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->search_rsp(
+ *btAddr, (btrc_status_t)rspStatus, (uint32_t)uidCounter,
+ (uint32_t)nItems);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed search_rsp, status: %d", status);
+ }
+
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean playItemRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint rspStatus) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->play_item_rsp(
+ *btAddr, (btrc_status_t)rspStatus);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed play_item_rsp, status: %d", status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean getTotalNumOfItemsRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint rspStatus,
+ jint uidCounter, jint numItems) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ uint32_t nItems = (uint32_t)numItems;
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->get_total_num_of_items_rsp(
+ *btAddr, (btrc_status_t)rspStatus, (uint32_t)uidCounter,
+ (uint32_t)nItems);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed get_total_num_of_items_rsp, status: %d", status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static jboolean addToNowPlayingRspNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint rspStatus) {
+ if (!sBluetoothAvrcpInterface) {
+ ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, NULL);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* btAddr = (RawAddress*)addr;
+ bt_status_t status = sBluetoothAvrcpInterface->add_to_now_playing_rsp(
+ *btAddr, (btrc_status_t)rspStatus);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("Failed add_to_now_playing_rsp, status: %d", status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
+static JNINativeMethod sMethods[] = {
+ {"classInitNative", "()V", (void*)classInitNative},
+ {"initNative", "()V", (void*)initNative},
+ {"cleanupNative", "()V", (void*)cleanupNative},
+ {"getPlayStatusRspNative", "([BIII)Z", (void*)getPlayStatusRspNative},
+ {"getElementAttrRspNative", "([BB[I[Ljava/lang/String;)Z",
+ (void*)getElementAttrRspNative},
+ {"registerNotificationRspPlayStatusNative", "(II)Z",
+ (void*)registerNotificationRspPlayStatusNative},
+ {"registerNotificationRspTrackChangeNative", "(I[B)Z",
+ (void*)registerNotificationRspTrackChangeNative},
+ {"registerNotificationRspPlayPosNative", "(II)Z",
+ (void*)registerNotificationRspPlayPosNative},
+ {"setVolumeNative", "(I)Z", (void*)setVolumeNative},
+
+ {"setAddressedPlayerRspNative", "([BI)Z",
+ (void*)setAddressedPlayerRspNative},
+
+ {"setBrowsedPlayerRspNative", "([BIBI[Ljava/lang/String;)Z",
+ (void*)setBrowsedPlayerRspNative},
+
+ {"mediaPlayerListRspNative", "([BIIBI[I[B[I[B[S[Ljava/lang/String;)Z",
+ (void*)mediaPlayerListRspNative},
+
+ {"getFolderItemsRspNative",
+ "([BISBI[B[B[B[B[Ljava/lang/String;[I[I[Ljava/lang/String;)Z",
+ (void*)getFolderItemsRspNative},
+
+ {"changePathRspNative", "([BII)Z", (void*)changePathRspNative},
+
+ {"getItemAttrRspNative", "([BIB[I[Ljava/lang/String;)Z",
+ (void*)getItemAttrRspNative},
+
+ {"playItemRspNative", "([BI)Z", (void*)playItemRspNative},
+
+ {"getTotalNumOfItemsRspNative", "([BIII)Z",
+ (void*)getTotalNumOfItemsRspNative},
+
+ {"searchRspNative", "([BIII)Z", (void*)searchRspNative},
+
+ {"addToNowPlayingRspNative", "([BI)Z", (void*)addToNowPlayingRspNative},
+
+ {"registerNotificationRspAddrPlayerChangedNative", "(III)Z",
+ (void*)registerNotificationRspAddrPlayerChangedNative},
+
+ {"registerNotificationRspAvalPlayerChangedNative", "(I)Z",
+ (void*)registerNotificationRspAvalPlayerChangedNative},
+
+ {"registerNotificationRspUIDsChangedNative", "(II)Z",
+ (void*)registerNotificationRspUIDsChangedNative},
+
+ {"registerNotificationRspNowPlayingChangedNative", "(I)Z",
+ (void*)registerNotificationRspNowPlayingChangedNative}};
+
+int register_com_android_bluetooth_avrcp(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/bluetooth/avrcp/Avrcp",
+ sMethods, NELEM(sMethods));
+}
+
+/* Helper function to copy attributes of item.
+ * Assumes that all items in response have same number of attributes
+ *
+ * returns true on succes, false otherwise.
+*/
+static bool copy_item_attributes(JNIEnv* env, jobject object,
+ btrc_folder_items_t* pitem,
+ jint* p_attributesIds,
+ jobjectArray attributesArray, int item_idx,
+ int attribCopiedIndex) {
+ bool success = true;
+
+ /* copy attributes of the item */
+ if (0 < pitem->media.num_attrs) {
+ int num_attrs = pitem->media.num_attrs;
+ ALOGI("%s num_attr = %d", __func__, num_attrs);
+ pitem->media.p_attrs = new btrc_element_attr_val_t[num_attrs];
+ if (!pitem->media.p_attrs) {
+ return false;
+ }
+
+ for (int tempAtrCount = 0; tempAtrCount < pitem->media.num_attrs;
+ ++tempAtrCount) {
+ pitem->media.p_attrs[tempAtrCount].attr_id =
+ p_attributesIds[attribCopiedIndex + tempAtrCount];
+
+ ScopedLocalRef<jstring> text(
+ env, (jstring)env->GetObjectArrayElement(
+ attributesArray, attribCopiedIndex + tempAtrCount));
+
+ if (!copy_jstring(pitem->media.p_attrs[tempAtrCount].text,
+ BTRC_MAX_ATTR_STR_LEN, text.get(), env)) {
+ success = false;
+ ALOGE("%s: failed to copy attributes", __func__);
+ break;
+ }
+ }
+ }
+ return success;
+}
+
+/* Helper function to copy String data from java to native
+ *
+ * returns true on succes, false otherwise
+ */
+static bool copy_jstring(uint8_t* str, int maxBytes, jstring jstr,
+ JNIEnv* env) {
+ if (str == NULL || jstr == NULL || env == NULL) return false;
+
+ memset(str, 0, maxBytes);
+ const char* p_str = env->GetStringUTFChars(jstr, NULL);
+ size_t len = strnlen(p_str, maxBytes - 1);
+ memcpy(str, p_str, len);
+
+ env->ReleaseStringUTFChars(jstr, p_str);
+ return true;
+}
+
+/* Helper function to cleanup items */
+static void cleanup_items(btrc_folder_items_t* p_items, int numItems) {
+ for (int item_idx = 0; item_idx < numItems; item_idx++) {
+ /* release memory for attributes in case item is media item */
+ if ((BTRC_ITEM_MEDIA == p_items[item_idx].item_type) &&
+ p_items[item_idx].media.p_attrs != NULL)
+ delete[] p_items[item_idx].media.p_attrs;
+ }
+}
+}