/* * Copyright (c) 2019 Livio, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following * disclaimer in the documentation and/or other materials provided with the * distribution. * * Neither the name of the Livio Inc. nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.smartdevicelink.util; import android.annotation.SuppressLint; 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.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.Build; import android.support.annotation.NonNull; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * Possible improvements * * - Narrow down list of acceptable audio devices * - Add ability to listen for when audio devices become available, and then connect * - Improve redundant calls to create String arrays for action arrays */ public class MediaStreamingStatus { private static final Object BROADCAST_RECEIVER_LOCK = new Object(); private boolean broadcastReceiverValid = true; private WeakReference<Context> contextWeakReference; private Callback callback; private List<String> intentList; public MediaStreamingStatus(@NonNull Context context, @NonNull Callback callback){ contextWeakReference = new WeakReference<>(context); this.callback = callback; intentList = new ArrayList<>(); //This is a default action that should be added intentList.add(AudioManager.ACTION_AUDIO_BECOMING_NOISY); } public void clear(){ callback = null; unregisterBroadcastReceiver(); contextWeakReference.clear(); } /* Working order --------------------------------------------------------------------------------------------- 1. If API level >= 23, use AudioManager to get connected audio output devices. Covers ~ 74.8% of Android devices as of 5/30/2019 This will return for a number of different supported audio devices. Full list can be seen in the isSupportedAudioDevice method. 2. If API level >= 3 && <=22, use the BluetoothManager to detect A2DP connection. Covers ~ 25.2% of Android devices not covered in option 1 as of 5/30/2019 This will enforce that bluetooth is connected as an audio output. No other type of audio device can currently be detected. 3. If API level <= 2, return false. Covers <1% of Android devices not covered by cases 1 and 2. Other options considered included: - BluetoothAdapter.getProfileConnectionState(BluetoothProfile.A2DP) == STATE_CONNECTED || STATE_CONNECTING ; - MediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO).getDeviceType() == MediaRouter.RouteInfo.DEVICE_TYPE_BLUETOOTH ; */ @SuppressLint("MissingPermission") public synchronized boolean isAudioOutputAvailable() { Context context = contextWeakReference.get(); if(context == null){ return false;} AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); // If API level 23+ audio manager can iterate over all current devices to see if a supported // device is present. if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); if(deviceInfos != null) { for (AudioDeviceInfo deviceInfo : deviceInfos) { if (deviceInfo != null && isSupportedAudioDevice(deviceInfo.getType())) { return true; } } } return false; } //This means the SDK version is < M, and our min is 8 so this API is always available return audioManager.isBluetoothA2dpOn(); } /** * This method will check to ensure that the device is supported. If possible, it will also * check against known variables and flags to ensure that that device is connected. This is * required as the AudioManager tends to be untrustworthy. * @param audioDevice * @return */ boolean isSupportedAudioDevice(int audioDevice){ DebugTool.logInfo("Audio device connected: " + audioDevice); switch (audioDevice){ case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP: if(isBluetoothActuallyAvailable()) { setupBluetoothBroadcastReceiver(); return true; //Make sure this doesn't fall to any other logic after this point } return false; case AudioDeviceInfo.TYPE_DOCK: case AudioDeviceInfo.TYPE_USB_ACCESSORY: case AudioDeviceInfo.TYPE_USB_DEVICE: case AudioDeviceInfo.TYPE_USB_HEADSET: if(isUsbActuallyConnected()) { setupUSBBroadcastReceiver(); return true; } return false; case AudioDeviceInfo.TYPE_LINE_ANALOG: case AudioDeviceInfo.TYPE_LINE_DIGITAL: case AudioDeviceInfo.TYPE_WIRED_HEADSET: case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: case AudioDeviceInfo.TYPE_AUX_LINE: setupHeadsetBroadcastReceiver(); return true; } return false; } @SuppressLint("MissingPermission") boolean isBluetoothActuallyAvailable(){ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if(adapter == null || !adapter.isEnabled() ){ //False positive return false; } if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH ){ int state = adapter.getProfileConnectionState(BluetoothProfile.A2DP); if(state != BluetoothAdapter.STATE_CONNECTING && state != BluetoothAdapter.STATE_CONNECTED){ //False positive return false; } } return true; } boolean isUsbActuallyConnected(){ Context context = contextWeakReference.get(); if(context != null){ return AndroidTools.isUSBCableConnected(context); } //default to true return true; } private void setupBluetoothBroadcastReceiver(){ String[] actions = new String[4]; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { actions[0] = BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED; }else{ actions[0] = "android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED"; } actions[1] = BluetoothAdapter.ACTION_STATE_CHANGED; actions[2] = BluetoothDevice.ACTION_ACL_DISCONNECTED; actions[3] = BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED; listenForIntents(actions); } private void setupHeadsetBroadcastReceiver(){ String[] actions = new String[1]; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { actions[0] = AudioManager.ACTION_HEADSET_PLUG; }else{ actions[0] = "android.intent.action.HEADSET_PLUG"; } listenForIntents(actions); } private void setupUSBBroadcastReceiver(){ String[] actions = new String[1]; actions[0] = Intent.ACTION_BATTERY_CHANGED; listenForIntents(actions); } private void listenForIntents(@NonNull String[] actions){ if(intentList != null){ //Add each intent int preAddSize = intentList.size(); for(String action : actions){ if(action != null && action.length() > 0 && !intentList.contains(action)){ intentList.add(action); } } if(preAddSize != intentList.size()){ synchronized (BROADCAST_RECEIVER_LOCK){ broadcastReceiverValid = true; } updateBroadcastReceiver(); } } } private void updateBroadcastReceiver() { //The broadcast receiver has not been setup for this yet Context context = contextWeakReference.get(); if (context != null) { IntentFilter intentFilter = new IntentFilter(); for (String intentAction : intentList) { intentFilter.addAction(intentAction); } unregisterBroadcastReceiver(); //Re-register receiver context.registerReceiver(broadcastReceiver, intentFilter); } } private void unregisterBroadcastReceiver(){ Context context = contextWeakReference.get(); if(context != null) { try{ context.unregisterReceiver(broadcastReceiver); }catch (Exception e){ //Ignore the exception } } } private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { synchronized (BROADCAST_RECEIVER_LOCK) { if (!isAudioOutputAvailable()) { if (broadcastReceiverValid) { broadcastReceiverValid = false; //No audio device is acceptable any longer if (callback != null) { callback.onAudioNoLongerAvailable(); } intentList.clear(); unregisterBroadcastReceiver(); } } } } }; public interface Callback{ void onAudioNoLongerAvailable(); } }