Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settingslib.bluetooth;
     18 
     19 import android.bluetooth.BluetoothA2dp;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothClass;
     22 import android.bluetooth.BluetoothCodecConfig;
     23 import android.bluetooth.BluetoothCodecStatus;
     24 import android.bluetooth.BluetoothDevice;
     25 import android.bluetooth.BluetoothProfile;
     26 import android.bluetooth.BluetoothUuid;
     27 import android.content.Context;
     28 import android.os.ParcelUuid;
     29 import android.util.Log;
     30 
     31 import com.android.internal.annotations.VisibleForTesting;
     32 import com.android.settingslib.R;
     33 import com.android.settingslib.wrapper.BluetoothA2dpWrapper;
     34 
     35 import java.util.ArrayList;
     36 import java.util.Arrays;
     37 import java.util.List;
     38 
     39 public class A2dpProfile implements LocalBluetoothProfile {
     40     private static final String TAG = "A2dpProfile";
     41     private static boolean V = false;
     42 
     43     private Context mContext;
     44 
     45     private BluetoothA2dp mService;
     46     private BluetoothA2dpWrapper mServiceWrapper;
     47     private boolean mIsProfileReady;
     48 
     49     private final LocalBluetoothAdapter mLocalAdapter;
     50     private final CachedBluetoothDeviceManager mDeviceManager;
     51 
     52     static final ParcelUuid[] SINK_UUIDS = {
     53         BluetoothUuid.AudioSink,
     54         BluetoothUuid.AdvAudioDist,
     55     };
     56 
     57     static final String NAME = "A2DP";
     58     private final LocalBluetoothProfileManager mProfileManager;
     59 
     60     // Order of this profile in device profiles list
     61     private static final int ORDINAL = 1;
     62 
     63     // These callbacks run on the main thread.
     64     private final class A2dpServiceListener
     65             implements BluetoothProfile.ServiceListener {
     66 
     67         public void onServiceConnected(int profile, BluetoothProfile proxy) {
     68             if (V) Log.d(TAG,"Bluetooth service connected");
     69             mService = (BluetoothA2dp) proxy;
     70             mServiceWrapper = new BluetoothA2dpWrapper(mService);
     71             // We just bound to the service, so refresh the UI for any connected A2DP devices.
     72             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
     73             while (!deviceList.isEmpty()) {
     74                 BluetoothDevice nextDevice = deviceList.remove(0);
     75                 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
     76                 // we may add a new device here, but generally this should not happen
     77                 if (device == null) {
     78                     Log.w(TAG, "A2dpProfile found new device: " + nextDevice);
     79                     device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice);
     80                 }
     81                 device.onProfileStateChanged(A2dpProfile.this, BluetoothProfile.STATE_CONNECTED);
     82                 device.refresh();
     83             }
     84             mIsProfileReady=true;
     85         }
     86 
     87         public void onServiceDisconnected(int profile) {
     88             if (V) Log.d(TAG,"Bluetooth service disconnected");
     89             mIsProfileReady=false;
     90         }
     91     }
     92 
     93     public boolean isProfileReady() {
     94         return mIsProfileReady;
     95     }
     96 
     97     @Override
     98     public int getProfileId() {
     99         return BluetoothProfile.A2DP;
    100     }
    101 
    102     A2dpProfile(Context context, LocalBluetoothAdapter adapter,
    103             CachedBluetoothDeviceManager deviceManager,
    104             LocalBluetoothProfileManager profileManager) {
    105         mContext = context;
    106         mLocalAdapter = adapter;
    107         mDeviceManager = deviceManager;
    108         mProfileManager = profileManager;
    109         mLocalAdapter.getProfileProxy(context, new A2dpServiceListener(),
    110                 BluetoothProfile.A2DP);
    111     }
    112 
    113     @VisibleForTesting
    114     void setBluetoothA2dpWrapper(BluetoothA2dpWrapper wrapper) {
    115         mServiceWrapper = wrapper;
    116     }
    117 
    118     public boolean isConnectable() {
    119         return true;
    120     }
    121 
    122     public boolean isAutoConnectable() {
    123         return true;
    124     }
    125 
    126     public List<BluetoothDevice> getConnectedDevices() {
    127         if (mService == null) return new ArrayList<BluetoothDevice>(0);
    128         return mService.getDevicesMatchingConnectionStates(
    129               new int[] {BluetoothProfile.STATE_CONNECTED,
    130                          BluetoothProfile.STATE_CONNECTING,
    131                          BluetoothProfile.STATE_DISCONNECTING});
    132     }
    133 
    134     public boolean connect(BluetoothDevice device) {
    135         if (mService == null) return false;
    136         int max_connected_devices = mLocalAdapter.getMaxConnectedAudioDevices();
    137         if (max_connected_devices == 1) {
    138             // Original behavior: disconnect currently connected device
    139             List<BluetoothDevice> sinks = getConnectedDevices();
    140             if (sinks != null) {
    141                 for (BluetoothDevice sink : sinks) {
    142                     if (sink.equals(device)) {
    143                         Log.w(TAG, "Connecting to device " + device + " : disconnect skipped");
    144                         continue;
    145                     }
    146                     mService.disconnect(sink);
    147                 }
    148             }
    149         }
    150         return mService.connect(device);
    151     }
    152 
    153     public boolean disconnect(BluetoothDevice device) {
    154         if (mService == null) return false;
    155         // Downgrade priority as user is disconnecting the headset.
    156         if (mService.getPriority(device) > BluetoothProfile.PRIORITY_ON){
    157             mService.setPriority(device, BluetoothProfile.PRIORITY_ON);
    158         }
    159         return mService.disconnect(device);
    160     }
    161 
    162     public int getConnectionStatus(BluetoothDevice device) {
    163         if (mService == null) {
    164             return BluetoothProfile.STATE_DISCONNECTED;
    165         }
    166         return mService.getConnectionState(device);
    167     }
    168 
    169     public boolean setActiveDevice(BluetoothDevice device) {
    170         if (mService == null) return false;
    171         return mService.setActiveDevice(device);
    172     }
    173 
    174     public BluetoothDevice getActiveDevice() {
    175         if (mService == null) return null;
    176         return mService.getActiveDevice();
    177     }
    178 
    179     public boolean isPreferred(BluetoothDevice device) {
    180         if (mService == null) return false;
    181         return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
    182     }
    183 
    184     public int getPreferred(BluetoothDevice device) {
    185         if (mService == null) return BluetoothProfile.PRIORITY_OFF;
    186         return mService.getPriority(device);
    187     }
    188 
    189     public void setPreferred(BluetoothDevice device, boolean preferred) {
    190         if (mService == null) return;
    191         if (preferred) {
    192             if (mService.getPriority(device) < BluetoothProfile.PRIORITY_ON) {
    193                 mService.setPriority(device, BluetoothProfile.PRIORITY_ON);
    194             }
    195         } else {
    196             mService.setPriority(device, BluetoothProfile.PRIORITY_OFF);
    197         }
    198     }
    199     boolean isA2dpPlaying() {
    200         if (mService == null) return false;
    201         List<BluetoothDevice> sinks = mService.getConnectedDevices();
    202         for (BluetoothDevice device : sinks) {
    203             if (mService.isA2dpPlaying(device)) {
    204                 return true;
    205             }
    206         }
    207         return false;
    208     }
    209 
    210     public boolean supportsHighQualityAudio(BluetoothDevice device) {
    211         int support = mServiceWrapper.supportsOptionalCodecs(device);
    212         return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED;
    213     }
    214 
    215     public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
    216         int enabled = mServiceWrapper.getOptionalCodecsEnabled(device);
    217         if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) {
    218             return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED;
    219         } else if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED &&
    220                 supportsHighQualityAudio(device)) {
    221             // Since we don't have a stored preference and the device isn't connected, just return
    222             // true since the default behavior when the device gets connected in the future would be
    223             // to have optional codecs enabled.
    224             return true;
    225         }
    226         BluetoothCodecConfig codecConfig = null;
    227         if (mServiceWrapper.getCodecStatus(device) != null) {
    228             codecConfig = mServiceWrapper.getCodecStatus(device).getCodecConfig();
    229         }
    230         if (codecConfig != null)  {
    231             return !codecConfig.isMandatoryCodec();
    232         } else {
    233             return false;
    234         }
    235     }
    236 
    237     public void setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled) {
    238         int prefValue = enabled
    239                 ? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED
    240                 : BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED;
    241         mServiceWrapper.setOptionalCodecsEnabled(device, prefValue);
    242         if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
    243             return;
    244         }
    245         if (enabled) {
    246             mService.enableOptionalCodecs(device);
    247         } else {
    248             mService.disableOptionalCodecs(device);
    249         }
    250     }
    251 
    252     public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
    253         int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
    254         if (!supportsHighQualityAudio(device)
    255                 || getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
    256             return mContext.getString(unknownCodecId);
    257         }
    258         // We want to get the highest priority codec, since that's the one that will be used with
    259         // this device, and see if it is high-quality (ie non-mandatory).
    260         BluetoothCodecConfig[] selectable = null;
    261         if (mServiceWrapper.getCodecStatus(device) != null) {
    262             selectable = mServiceWrapper.getCodecStatus(device).getCodecsSelectableCapabilities();
    263             // To get the highest priority, we sort in reverse.
    264             Arrays.sort(selectable,
    265                     (a, b) -> {
    266                         return b.getCodecPriority() - a.getCodecPriority();
    267                     });
    268         }
    269 
    270         final BluetoothCodecConfig codecConfig = (selectable == null || selectable.length < 1)
    271                 ? null : selectable[0];
    272         final int codecType = (codecConfig == null || codecConfig.isMandatoryCodec())
    273                 ? BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID : codecConfig.getCodecType();
    274 
    275         int index = -1;
    276         switch (codecType) {
    277            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
    278                index = 1;
    279                break;
    280            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
    281                index = 2;
    282                break;
    283            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
    284                index = 3;
    285                break;
    286            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
    287                index = 4;
    288                break;
    289            case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
    290                index = 5;
    291                break;
    292            }
    293 
    294         if (index < 0) {
    295             return mContext.getString(unknownCodecId);
    296         }
    297         return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality,
    298                 mContext.getResources().getStringArray(R.array.bluetooth_a2dp_codec_titles)[index]);
    299     }
    300 
    301     public String toString() {
    302         return NAME;
    303     }
    304 
    305     public int getOrdinal() {
    306         return ORDINAL;
    307     }
    308 
    309     public int getNameResource(BluetoothDevice device) {
    310         return R.string.bluetooth_profile_a2dp;
    311     }
    312 
    313     public int getSummaryResourceForDevice(BluetoothDevice device) {
    314         int state = getConnectionStatus(device);
    315         switch (state) {
    316             case BluetoothProfile.STATE_DISCONNECTED:
    317                 return R.string.bluetooth_a2dp_profile_summary_use_for;
    318 
    319             case BluetoothProfile.STATE_CONNECTED:
    320                 return R.string.bluetooth_a2dp_profile_summary_connected;
    321 
    322             default:
    323                 return Utils.getConnectionStateSummary(state);
    324         }
    325     }
    326 
    327     public int getDrawableResource(BluetoothClass btClass) {
    328         return R.drawable.ic_bt_headphones_a2dp;
    329     }
    330 
    331     protected void finalize() {
    332         if (V) Log.d(TAG, "finalize()");
    333         if (mService != null) {
    334             try {
    335                 BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP,
    336                                                                        mService);
    337                 mService = null;
    338             }catch (Throwable t) {
    339                 Log.w(TAG, "Error cleaning up A2DP proxy", t);
    340             }
    341         }
    342     }
    343 }
    344