OSDN Git Service

Add MediaBrowser implementaion for A2DP.
authorSanket Agarwal <sanketa@google.com>
Fri, 22 Jan 2016 20:26:36 +0000 (12:26 -0800)
committerAndre Eisenbach <eisenbach@google.com>
Tue, 2 Feb 2016 19:37:21 +0000 (19:37 +0000)
MediaBrowser is a way to control playback of any generic media
application and to conform to this framework API the a2dpsink client
uses the same format.

This middleware provides a way to use A2DP via the MediaBrowserService
and a user for A2DP sink should not need to use the raw Bluetooth APIs.

Change-Id: I73b4a51acc469f503286bfd6d33a1766e30fc50d

services/A2dpMediaBrowserService/Android.mk [new file with mode: 0644]
services/A2dpMediaBrowserService/AndroidManifest.xml [new file with mode: 0644]
services/A2dpMediaBrowserService/res/drawable-hdpi/ic_launcher.png [new file with mode: 0644]
services/A2dpMediaBrowserService/res/drawable-hdpi/ic_notification.png [new file with mode: 0644]
services/A2dpMediaBrowserService/res/values-v21/styles.xml [new file with mode: 0644]
services/A2dpMediaBrowserService/res/values/strings.xml [new file with mode: 0644]
services/A2dpMediaBrowserService/res/values/styles.xml [new file with mode: 0644]
services/A2dpMediaBrowserService/res/xml/automotive_app_desc.xml [new file with mode: 0644]
services/A2dpMediaBrowserService/src/com/google/android/a2dpsink/mbs/A2dpMediaBrowserService.java [new file with mode: 0644]
services/A2dpMediaBrowserService/src/com/google/android/a2dpsink/mbs/BootCompleteReceiver.java [new file with mode: 0644]

diff --git a/services/A2dpMediaBrowserService/Android.mk b/services/A2dpMediaBrowserService/Android.mk
new file mode 100644 (file)
index 0000000..f7a5e97
--- /dev/null
@@ -0,0 +1,19 @@
+# Copyright 2015 Google Inc. All Rights Reserved.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the application.
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := A2dpMediaBrowserService
+LOCAL_MODULE_TAGS := optional
+LOCAL_CERTIFICATE := platform
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR = $(LOCAL_PATH)/res
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/services/A2dpMediaBrowserService/AndroidManifest.xml b/services/A2dpMediaBrowserService/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..fa63ff4
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2015, 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.a2dpsink.mbs" >
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
+
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+
+    <application
+        android:allowBackup="false"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme" >
+        <meta-data android:name="com.google.android.gms.car.application"
+            android:resource="@xml/automotive_app_desc" />
+
+        <meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
+            android:resource="@drawable/ic_notification" />
+
+        <service android:name=".A2dpMediaBrowserService" android:exported="true">
+            <intent-filter>
+                <action android:name="android.media.browse.MediaBrowserService" />
+            </intent-filter>
+        </service>
+
+        <receiver android:name=".BootCompleteReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+
+    </application>
+</manifest>
diff --git a/services/A2dpMediaBrowserService/res/drawable-hdpi/ic_launcher.png b/services/A2dpMediaBrowserService/res/drawable-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..9075ff1
Binary files /dev/null and b/services/A2dpMediaBrowserService/res/drawable-hdpi/ic_launcher.png differ
diff --git a/services/A2dpMediaBrowserService/res/drawable-hdpi/ic_notification.png b/services/A2dpMediaBrowserService/res/drawable-hdpi/ic_notification.png
new file mode 100644 (file)
index 0000000..9075ff1
Binary files /dev/null and b/services/A2dpMediaBrowserService/res/drawable-hdpi/ic_notification.png differ
diff --git a/services/A2dpMediaBrowserService/res/values-v21/styles.xml b/services/A2dpMediaBrowserService/res/values-v21/styles.xml
new file mode 100644 (file)
index 0000000..7caef1f
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2015, 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.
+-->
+<resources>
+    <style name="AppBaseTheme" parent="android:Theme.Material">
+        <item name="android:colorPrimary">#ffff5722</item>
+        <item name="android:colorPrimaryDark">#ffbf360c</item>
+        <item name="android:colorAccent">#ffff5722</item>
+    </style>
+</resources>
diff --git a/services/A2dpMediaBrowserService/res/values/strings.xml b/services/A2dpMediaBrowserService/res/values/strings.xml
new file mode 100644 (file)
index 0000000..588a124
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2015, 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.
+-->
+<resources>
+        <string name="app_name">Bluetooth Audio</string>
+        <string name="bluetooth_connected">Bluetooth audio connected</string>
+        <string name="bluetooth_disconnected">Bluetooth audio disconnected"</string>
+</resources>
diff --git a/services/A2dpMediaBrowserService/res/values/styles.xml b/services/A2dpMediaBrowserService/res/values/styles.xml
new file mode 100644 (file)
index 0000000..45c2579
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2015, 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.
+-->
+<resources>
+    <style name="AppTheme" parent="AppBaseTheme" />
+    <style name="AppBaseTheme" parent="android:Theme.Light" />
+</resources>
diff --git a/services/A2dpMediaBrowserService/res/xml/automotive_app_desc.xml b/services/A2dpMediaBrowserService/res/xml/automotive_app_desc.xml
new file mode 100644 (file)
index 0000000..a5bac63
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2015, 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.
+-->
+<automotiveApp>
+    <uses name="media"/>
+</automotiveApp>
diff --git a/services/A2dpMediaBrowserService/src/com/google/android/a2dpsink/mbs/A2dpMediaBrowserService.java b/services/A2dpMediaBrowserService/src/com/google/android/a2dpsink/mbs/A2dpMediaBrowserService.java
new file mode 100644 (file)
index 0000000..2a6baa8
--- /dev/null
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2015 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.google.android.a2dpsink.mbs;
+
+import android.bluetooth.BluetoothA2dpSink;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAvrcpController;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.MediaMetadata;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.service.media.MediaBrowserService;
+import android.util.Pair;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+public class A2dpMediaBrowserService extends MediaBrowserService {
+    private static final String TAG = "A2dpMediaBrowserService";
+    private static final String MEDIA_ID_ROOT = "__ROOT__";
+    private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__";
+    private static final float PLAYBACK_SPEED = 1.0f;
+
+    // Message sent when A2DP device is disconnected.
+    private static final int MSG_DEVICE_DISCONNECT = 0;
+    // Message snet when the AVRCP profile is disconnected = 1;
+    private static final int MSG_PROFILE_DISCONNECT = 1;
+    // Message sent when A2DP device is connected.
+    private static final int MSG_DEVICE_CONNECT = 2;
+    // Message sent when AVRCP profile is connected (note AVRCP profile may be connected before or
+    // after A2DP device is connected).
+    private static final int MSG_PROFILE_CONNECT = 3;
+    // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device.
+    private static final int MSG_TRACK = 4;
+    // Internal message sent to trigger a AVRCP action.
+    private static final int MSG_AVRCP_PASSTHRU = 5;
+
+    private MediaSession mSession;
+    private MediaMetadata mA2dpMetadata;
+
+    private BluetoothAdapter mAdapter;
+    private BluetoothAvrcpController mAvrcpProfile;
+    private BluetoothDevice mA2dpDevice = null;
+    private Handler mAvrcpCommandQueue;
+
+    private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
+            | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+
+    private static final class AvrcpCommandQueueHandler extends Handler {
+        WeakReference<A2dpMediaBrowserService> mInst;
+
+        AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) {
+            super(looper);
+            mInst = new WeakReference<A2dpMediaBrowserService>(sink);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            A2dpMediaBrowserService inst = mInst.get();
+            if (inst == null) {
+                Log.e(TAG, "Parent class has died; aborting.");
+                return;
+            }
+
+            switch (msg.what) {
+                case MSG_DEVICE_CONNECT:
+                    inst.msgDeviceConnect((BluetoothDevice) msg.obj);
+                    break;
+                case MSG_PROFILE_CONNECT:
+                    inst.msgProfileConnect((BluetoothProfile) msg.obj);
+                    break;
+                case MSG_DEVICE_DISCONNECT:
+                    inst.msgDeviceDisconnect((BluetoothDevice) msg.obj);
+                    break;
+                case MSG_PROFILE_DISCONNECT:
+                    inst.msgProfileDisconnect();
+                    break;
+                case MSG_TRACK:
+                    Pair<PlaybackState, MediaMetadata> pair =
+                        (Pair<PlaybackState, MediaMetadata>) (msg.obj);
+                    inst.msgTrack(pair.first, pair.second);
+                    break;
+                case MSG_AVRCP_PASSTHRU:
+                    inst.msgPassThru((int) msg.obj);
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        Log.d(TAG, "onCreate");
+        super.onCreate();
+        mSession = new MediaSession(this, TAG);
+        setSessionToken(mSession.getSessionToken());
+        mSession.setCallback(mSessionCallbacks);
+        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
+                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
+        mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.AVRCP_CONTROLLER);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
+        filter.addAction(BluetoothAvrcpController.ACTION_TRACK_EVENT);
+        registerReceiver(mBtReceiver, filter);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(TAG, "onDestroy");
+        mSession.release();
+        unregisterReceiver(mBtReceiver);
+        super.onDestroy();
+    }
+
+    @Override
+    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+        return new BrowserRoot(MEDIA_ID_ROOT, null);
+    }
+
+    @Override
+    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+        Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
+        List<MediaItem> items = new ArrayList<MediaItem>();
+        result.sendResult(items);
+    }
+
+    BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() {
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            Log.d(TAG, "onServiceConnected");
+            if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
+                mAvrcpCommandQueue.obtainMessage(MSG_PROFILE_CONNECT, proxy).sendToTarget();
+                List<BluetoothDevice> devices = proxy.getConnectedDevices();
+                if (devices != null && devices.size() > 0) {
+                    BluetoothDevice device = devices.get(0);
+                    Log.d(TAG, "got AVRCP device " + device);
+                }
+            }
+        }
+
+        public void onServiceDisconnected(int profile) {
+            Log.d(TAG, "onServiceDisconnected " + profile);
+            if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
+                mAvrcpProfile = null;
+                mAvrcpCommandQueue.obtainMessage(MSG_PROFILE_DISCONNECT).sendToTarget();
+            }
+        }
+    };
+
+    // Media Session Stuff.
+    private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
+        @Override
+        public void onPlay() {
+            Log.d(TAG, "onPlay");
+            mAvrcpCommandQueue.obtainMessage(
+                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_PLAY).sendToTarget();
+            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
+        }
+
+        @Override
+        public void onPause() {
+            Log.d(TAG, "onPause");
+            mAvrcpCommandQueue.obtainMessage(
+                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_PAUSE).sendToTarget();
+            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
+        }
+
+        @Override
+        public void onSkipToNext() {
+            Log.d(TAG, "onSkipToNext");
+            mAvrcpCommandQueue.obtainMessage(
+                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_FORWARD)
+                .sendToTarget();
+            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            Log.d(TAG, "onSkipToPrevious");
+
+            mAvrcpCommandQueue.obtainMessage(
+                MSG_AVRCP_PASSTHRU, BluetoothAvrcpController.PASS_THRU_CMD_ID_BACKWARD)
+                .sendToTarget();
+            // TRACK_EVENT should be fired eventually and the UI should be hence updated.
+        }
+
+        // These are not yet supported.
+        @Override
+        public void onStop() {
+            Log.d(TAG, "onStop");
+        }
+
+        @Override
+        public void onCustomAction(String action, Bundle extras) {
+            Log.d(TAG, "onCustomAction action=" + action + " extras=" + extras);
+        }
+
+        @Override
+        public void onPlayFromSearch(String query, Bundle extras) {
+            Log.d(TAG, "playFromSearch not supported in AVRCP");
+        }
+
+        @Override
+        public void onCommand(String command, Bundle args, ResultReceiver cb) {
+            Log.d(TAG, "onCommand command=" + command + " args=" + args);
+        }
+
+        @Override
+        public void onSkipToQueueItem(long queueId) {
+            Log.d(TAG, "onSkipToQueueItem");
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            Log.d(TAG, "onPlayFromMediaId mediaId=" + mediaId + " extras=" + extras);
+        }
+
+    };
+
+    private BroadcastReceiver mBtReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.d(TAG, "onReceive intent=" + intent);
+            String action = intent.getAction();
+            BluetoothDevice btDev =
+                    (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+            if (BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+                Log.d(TAG, "handleConnectionStateChange: newState="
+                        + state + " btDev=" + btDev);
+
+                // Connected state will be handled when AVRCP BluetoothProfile gets connected.
+                if (state == BluetoothProfile.STATE_CONNECTED) {
+                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget();
+                } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
+                    // Set the playback state to unconnected.
+                    mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget();
+                }
+            } else if (BluetoothAvrcpController.ACTION_TRACK_EVENT.equals(action)) {
+                PlaybackState pbb =
+                    intent.getParcelableExtra(BluetoothAvrcpController.EXTRA_PLAYBACK);
+                MediaMetadata mmd =
+                    intent.getParcelableExtra(BluetoothAvrcpController.EXTRA_METADATA);
+                mAvrcpCommandQueue.obtainMessage(
+                    MSG_TRACK, new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget();
+            }
+        }
+    };
+
+    private void msgDeviceConnect(BluetoothDevice device) {
+        Log.d(TAG, "msgDeviceConnect");
+        // We are connected to a new device via A2DP now.
+        mA2dpDevice = device;
+    }
+
+    private void msgProfileConnect(BluetoothProfile profile) {
+        Log.d(TAG, "msgProfileConnect");
+        if (profile != null) {
+            mAvrcpProfile = (BluetoothAvrcpController) profile;
+        }
+
+        List<BluetoothDevice> devices = mAvrcpProfile.getConnectedDevices();
+        if (devices.size() == 0) {
+            Log.w(TAG, "No devices connected yet");
+            return;
+        }
+
+        if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) {
+            Log.e(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0));
+        }
+        mA2dpDevice = devices.get(0);
+
+        PlaybackState playbackState = mAvrcpProfile.getPlaybackState(mA2dpDevice);
+        // Add actions required for playback and rebuild the object.
+        PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState);
+        playbackState = pbb.setActions(mTransportControlFlags).build();
+
+        MediaMetadata mediaMetadata = mAvrcpProfile.getMetadata(mA2dpDevice);
+        Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState);
+        mSession.setMetadata(mAvrcpProfile.getMetadata(mA2dpDevice));
+        mSession.setPlaybackState(playbackState);
+    }
+
+    private void msgDeviceDisconnect(BluetoothDevice device) {
+        Log.d(TAG, "msgDeviceDisconnect");
+        if (mA2dpDevice == null) {
+            Log.w(TAG, "Already disconnected - nothing to do here.");
+            return;
+        } else if (!mA2dpDevice.equals(device)) {
+            Log.e(TAG, "Not the right device to disconnect current " +
+                mA2dpDevice + " dc " + device);
+            return;
+        }
+
+        // Unset the session.
+        PlaybackState.Builder pbb = new PlaybackState.Builder();
+        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
+                    PLAYBACK_SPEED)
+                .setActions(mTransportControlFlags)
+                .setErrorMessage(getString(R.string.bluetooth_disconnected));
+        mSession.setPlaybackState(pbb.build());
+    }
+
+    private void msgProfileDisconnect() {
+        Log.d(TAG, "msgProfileDisconnect");
+        // The profile is disconnected - even if the device is still connected we cannot really have
+        // a functioning UI so reset the session.
+        mAvrcpProfile = null;
+
+        // Unset the session.
+        PlaybackState.Builder pbb = new PlaybackState.Builder();
+        pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
+                    PLAYBACK_SPEED)
+                .setActions(mTransportControlFlags)
+                .setErrorMessage(getString(R.string.bluetooth_disconnected));
+        mSession.setPlaybackState(pbb.build());
+    }
+
+    private void msgTrack(PlaybackState pb, MediaMetadata mmd) {
+        Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd);
+        if (mmd != null) {
+            Log.d(TAG, "msgTrack() mmd " + mmd.getDescription());
+            mSession.setMetadata(mmd);
+        }
+
+        if (pb != null) {
+            Log.d(TAG, "msgTrack() playbackstate " + pb);
+            PlaybackState.Builder pbb = new PlaybackState.Builder(pb);
+            pb = pbb.setActions(mTransportControlFlags).build();
+            mSession.setPlaybackState(pb);
+        }
+    }
+
+    private void msgPassThru(int cmd) {
+        Log.d(TAG, "msgPassThru " + cmd);
+        if (mA2dpDevice == null) {
+            // We should have already disconnected - ignore this message.
+            Log.e(TAG, "Already disconnected ignoring.");
+            return;
+        }
+
+        if (mAvrcpProfile == null) {
+            // We may be disconnected with the profile but there is not much we can do for now but
+            // to wait for the profile to come back up.
+            Log.e(TAG, "Profile disconnected; ignoring.");
+            return;
+        }
+
+        // Send the pass through.
+        mAvrcpProfile.sendPassThroughCmd(
+            mA2dpDevice, cmd, BluetoothAvrcpController.KEY_STATE_PRESSED);
+        mAvrcpProfile.sendPassThroughCmd(
+            mA2dpDevice, cmd, BluetoothAvrcpController.KEY_STATE_RELEASED);
+    }
+}
diff --git a/services/A2dpMediaBrowserService/src/com/google/android/a2dpsink/mbs/BootCompleteReceiver.java b/services/A2dpMediaBrowserService/src/com/google/android/a2dpsink/mbs/BootCompleteReceiver.java
new file mode 100644 (file)
index 0000000..0c86b84
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 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.google.android.a2dpsink.mbs;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class BootCompleteReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // We want to keep a track of bluetooth state so start up the service
+        // on boot and keep making a note of all bluetooth related state changes.
+        Intent startIntent = new Intent(context, A2dpMediaBrowserService.class);
+        context.startService(startIntent);
+    }
+}