/* * Copyright 2017, 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.example.androidthings.bluetooth.audio; import android.app.Activity; import android.bluetooth.BluetoothAdapter; 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.os.Bundle; import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.KeyEvent; import com.google.android.things.bluetooth.BluetoothProfileManager; import com.google.android.things.contrib.driver.button.Button; import com.google.android.things.contrib.driver.button.ButtonInputDriver; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; /** * Sample usage of the A2DP sink bluetooth profile. At startup, this activity sets the Bluetooth * adapter in pairing mode for {@link #DISCOVERABLE_TIMEOUT_MS} ms. * * To re-enable pairing mode, press "p" on an attached keyboard, use "adb shell input keyevent 44" * or press a button attached to the GPIO pin returned by {@link BoardDefaults#getGPIOForPairing()} * * To forcefully disconnect any connected A2DP device, press "d" on an attached keyboard, use * "adb shell input keyevent 32" or press a button attached to the GPIO pin * returned by {@link BoardDefaults#getGPIOForDisconnectAllBTDevices()} * * NOTE: While in pairing mode, pairing requests are auto-accepted - at this moment there's no * way to block specific pairing attempts while in pairing mode. This is known limitation that is * being worked on. * */ public class A2dpSinkActivity extends Activity { private static final String TAG = "A2dpSinkActivity"; private static final String ADAPTER_FRIENDLY_NAME = "My Android Things device"; private static final int DISCOVERABLE_TIMEOUT_MS = 300; private static final int REQUEST_CODE_ENABLE_DISCOVERABLE = 100; private static final String UTTERANCE_ID = "com.example.androidthings.bluetooth.audio.UTTERANCE_ID"; private BluetoothAdapter mBluetoothAdapter; private BluetoothProfile mA2DPSinkProxy; private ButtonInputDriver mPairingButtonDriver; private ButtonInputDriver mDisconnectAllButtonDriver; private TextToSpeech mTtsEngine; /** * Handle an intent that is broadcast by the Bluetooth adapter whenever it changes its * state (after calling enable(), for example). * Action is {@link BluetoothAdapter#ACTION_STATE_CHANGED} and extras describe the old * and the new states. You can use this intent to indicate that the device is ready to go. */ private final BroadcastReceiver mAdapterStateChangeReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { int oldState = A2dpSinkHelper.getPreviousAdapterState(intent); int newState = A2dpSinkHelper.getCurrentAdapterState(intent); Log.d(TAG, "Bluetooth Adapter changing state from " + oldState + " to " + newState); if (newState == BluetoothAdapter.STATE_ON) { Log.i(TAG, "Bluetooth Adapter is ready"); initA2DPSink(); } } }; /** * Handle an intent that is broadcast by the Bluetooth A2DP sink profile whenever a device * connects or disconnects to it. * Action is {@link A2dpSinkHelper#ACTION_CONNECTION_STATE_CHANGED} and * extras describe the old and the new connection states. You can use it to indicate that * there's a device connected. */ private final BroadcastReceiver mSinkProfileStateChangeReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(A2dpSinkHelper.ACTION_CONNECTION_STATE_CHANGED)) { int oldState = A2dpSinkHelper.getPreviousProfileState(intent); int newState = A2dpSinkHelper.getCurrentProfileState(intent); BluetoothDevice device = A2dpSinkHelper.getDevice(intent); Log.d(TAG, "Bluetooth A2DP sink changing connection state from " + oldState + " to " + newState + " device " + device); if (device != null) { String deviceName = Objects.toString(device.getName(), "a device"); if (newState == BluetoothProfile.STATE_CONNECTED) { speak("Connected to " + deviceName); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { speak("Disconnected from " + deviceName); } } } } }; /** * Handle an intent that is broadcast by the Bluetooth A2DP sink profile whenever a device * starts or stops playing through the A2DP sink. * Action is {@link A2dpSinkHelper#ACTION_PLAYING_STATE_CHANGED} and * extras describe the old and the new playback states. You can use it to indicate that * there's something playing. You don't need to handle the stream playback by yourself. */ private final BroadcastReceiver mSinkProfilePlaybackChangeReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(A2dpSinkHelper.ACTION_PLAYING_STATE_CHANGED)) { int oldState = A2dpSinkHelper.getPreviousProfileState(intent); int newState = A2dpSinkHelper.getCurrentProfileState(intent); BluetoothDevice device = A2dpSinkHelper.getDevice(intent); Log.d(TAG, "Bluetooth A2DP sink changing playback state from " + oldState + " to " + newState + " device " + device); if (device != null) { if (newState == A2dpSinkHelper.STATE_PLAYING) { Log.i(TAG, "Playing audio from device " + device.getAddress()); } else if (newState == A2dpSinkHelper.STATE_NOT_PLAYING) { Log.i(TAG, "Stopped playing audio from " + device.getAddress()); } } } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null) { Log.w(TAG, "No default Bluetooth adapter. Device likely does not support bluetooth."); return; } // We use Text-to-Speech to indicate status change to the user initTts(); registerReceiver(mAdapterStateChangeReceiver, new IntentFilter( BluetoothAdapter.ACTION_STATE_CHANGED)); registerReceiver(mSinkProfileStateChangeReceiver, new IntentFilter( A2dpSinkHelper.ACTION_CONNECTION_STATE_CHANGED)); registerReceiver(mSinkProfilePlaybackChangeReceiver, new IntentFilter( A2dpSinkHelper.ACTION_PLAYING_STATE_CHANGED)); if (mBluetoothAdapter.isEnabled()) { Log.d(TAG, "Bluetooth Adapter is already enabled."); initA2DPSink(); } else { Log.d(TAG, "Bluetooth adapter not enabled. Enabling."); mBluetoothAdapter.enable(); } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_P: // Enable Pairing mode (discoverable) enableDiscoverable(); return true; case KeyEvent.KEYCODE_D: // Disconnect any currently connected devices disconnectConnectedDevices(); return true; } return super.onKeyUp(keyCode, event); } @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); try { if (mPairingButtonDriver != null) mPairingButtonDriver.close(); } catch (IOException e) { /* close quietly */} try { if (mDisconnectAllButtonDriver != null) mDisconnectAllButtonDriver.close(); } catch (IOException e) { /* close quietly */} unregisterReceiver(mAdapterStateChangeReceiver); unregisterReceiver(mSinkProfileStateChangeReceiver); unregisterReceiver(mSinkProfilePlaybackChangeReceiver); if (mA2DPSinkProxy != null) { mBluetoothAdapter.closeProfileProxy(A2dpSinkHelper.A2DP_SINK_PROFILE, mA2DPSinkProxy); } if (mTtsEngine != null) { mTtsEngine.stop(); mTtsEngine.shutdown(); } // we intentionally leave the Bluetooth adapter enabled, so that other samples can use it // without having to initialize it. } private void setupBTProfiles() { BluetoothProfileManager bluetoothProfileManager = BluetoothProfileManager.getInstance(); List<Integer> enabledProfiles = bluetoothProfileManager.getEnabledProfiles(); if (!enabledProfiles.contains(A2dpSinkHelper.A2DP_SINK_PROFILE)) { Log.d(TAG, "Enabling A2dp sink mode."); List<Integer> toDisable = Arrays.asList(BluetoothProfile.A2DP); List<Integer> toEnable = Arrays.asList( A2dpSinkHelper.A2DP_SINK_PROFILE, A2dpSinkHelper.AVRCP_CONTROLLER_PROFILE); bluetoothProfileManager.enableAndDisableProfiles(toEnable, toDisable); } else { Log.d(TAG, "A2dp sink profile is enabled."); } } /** * Initiate the A2DP sink. */ private void initA2DPSink() { if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { Log.e(TAG, "Bluetooth adapter not available or not enabled."); return; } setupBTProfiles(); Log.d(TAG, "Set up Bluetooth Adapter name and profile"); mBluetoothAdapter.setName(ADAPTER_FRIENDLY_NAME); mBluetoothAdapter.getProfileProxy(this, new BluetoothProfile.ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { mA2DPSinkProxy = proxy; enableDiscoverable(); } @Override public void onServiceDisconnected(int profile) { } }, A2dpSinkHelper.A2DP_SINK_PROFILE); configureButton(); } /** * Enable the current {@link BluetoothAdapter} to be discovered (available for pairing) for * the next {@link #DISCOVERABLE_TIMEOUT_MS} ms. */ private void enableDiscoverable() { Log.d(TAG, "Registering for discovery."); Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, DISCOVERABLE_TIMEOUT_MS); startActivityForResult(discoverableIntent, REQUEST_CODE_ENABLE_DISCOVERABLE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_ENABLE_DISCOVERABLE) { Log.d(TAG, "Enable discoverable returned with result " + resultCode); // ResultCode, as described in BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE, is either // RESULT_CANCELED or the number of milliseconds that the device will stay in // discoverable mode. In a regular Android device, the user will see a popup requesting // authorization, and if they cancel, RESULT_CANCELED is returned. In Android Things, // on the other hand, the authorization for pairing is always given without user // interference, so RESULT_CANCELED should never be returned. if (resultCode == RESULT_CANCELED) { Log.e(TAG, "Enable discoverable has been cancelled by the user. " + "This should never happen in an Android Things device."); return; } Log.i(TAG, "Bluetooth adapter successfully set to discoverable mode. " + "Any A2DP source can find it with the name " + ADAPTER_FRIENDLY_NAME + " and pair for the next " + DISCOVERABLE_TIMEOUT_MS + " ms. " + "Try looking for it on your phone, for example."); // There is nothing else required here, since Android framework automatically handles // A2DP Sink. Most relevant Bluetooth events, like connection/disconnection, will // generate corresponding broadcast intents or profile proxy events that you can // listen to and react appropriately. speak("Bluetooth audio sink is discoverable for " + DISCOVERABLE_TIMEOUT_MS + " milliseconds. Look for a device named " + ADAPTER_FRIENDLY_NAME); } } private void disconnectConnectedDevices() { if (mA2DPSinkProxy == null || mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { return; } speak("Disconnecting devices"); for (BluetoothDevice device: mA2DPSinkProxy.getConnectedDevices()) { Log.i(TAG, "Disconnecting device " + device); A2dpSinkHelper.disconnect(mA2DPSinkProxy, device); } } private void configureButton() { try { mPairingButtonDriver = new ButtonInputDriver(BoardDefaults.getGPIOForPairing(), Button.LogicState.PRESSED_WHEN_LOW, KeyEvent.KEYCODE_P); mPairingButtonDriver.register(); mDisconnectAllButtonDriver = new ButtonInputDriver( BoardDefaults.getGPIOForDisconnectAllBTDevices(), Button.LogicState.PRESSED_WHEN_LOW, KeyEvent.KEYCODE_D); mDisconnectAllButtonDriver.register(); } catch (IOException e) { Log.w(TAG, "Could not register GPIO button drivers. Use keyboard events to trigger " + "the functions instead", e); } } private void initTts() { mTtsEngine = new TextToSpeech(A2dpSinkActivity.this, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { mTtsEngine.setLanguage(Locale.US); } else { Log.w(TAG, "Could not open TTS Engine (onInit status=" + status + "). Ignoring text to speech"); mTtsEngine = null; } } }); } private void speak(String utterance) { Log.i(TAG, utterance); if (mTtsEngine != null) { mTtsEngine.speak(utterance, TextToSpeech.QUEUE_ADD, null, UTTERANCE_ID); } } }