Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2008 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.settings.bluetooth;
     18 
     19 import com.android.settings.R;
     20 
     21 import android.bluetooth.BluetoothA2dp;
     22 import android.bluetooth.BluetoothDevice;
     23 import android.bluetooth.BluetoothHeadset;
     24 import android.bluetooth.BluetoothUuid;
     25 import android.os.Handler;
     26 import android.os.ParcelUuid;
     27 import android.util.Log;
     28 
     29 import java.util.HashMap;
     30 import java.util.HashSet;
     31 import java.util.Iterator;
     32 import java.util.LinkedList;
     33 import java.util.List;
     34 import java.util.Map;
     35 import java.util.Set;
     36 
     37 /**
     38  * LocalBluetoothProfileManager is an abstract class defining the basic
     39  * functionality related to a profile.
     40  */
     41 public abstract class LocalBluetoothProfileManager {
     42     private static final String TAG = "LocalBluetoothProfileManager";
     43 
     44     /* package */ static final ParcelUuid[] HEADSET_PROFILE_UUIDS = new ParcelUuid[] {
     45         BluetoothUuid.HSP,
     46         BluetoothUuid.Handsfree,
     47     };
     48 
     49     /* package */ static final ParcelUuid[] A2DP_PROFILE_UUIDS = new ParcelUuid[] {
     50         BluetoothUuid.AudioSink,
     51         BluetoothUuid.AdvAudioDist,
     52     };
     53 
     54     /* package */ static final ParcelUuid[] OPP_PROFILE_UUIDS = new ParcelUuid[] {
     55         BluetoothUuid.ObexObjectPush
     56     };
     57 
     58     /**
     59      * An interface for notifying BluetoothHeadset IPC clients when they have
     60      * been connected to the BluetoothHeadset service.
     61      */
     62     public interface ServiceListener {
     63         /**
     64          * Called to notify the client when this proxy object has been
     65          * connected to the BluetoothHeadset service. Clients must wait for
     66          * this callback before making IPC calls on the BluetoothHeadset
     67          * service.
     68          */
     69         public void onServiceConnected();
     70 
     71         /**
     72          * Called to notify the client that this proxy object has been
     73          * disconnected from the BluetoothHeadset service. Clients must not
     74          * make IPC calls on the BluetoothHeadset service after this callback.
     75          * This callback will currently only occur if the application hosting
     76          * the BluetoothHeadset service, but may be called more often in future.
     77          */
     78         public void onServiceDisconnected();
     79     }
     80 
     81     // TODO: close profiles when we're shutting down
     82     private static Map<Profile, LocalBluetoothProfileManager> sProfileMap =
     83             new HashMap<Profile, LocalBluetoothProfileManager>();
     84 
     85     protected LocalBluetoothManager mLocalManager;
     86 
     87     public static void init(LocalBluetoothManager localManager) {
     88         synchronized (sProfileMap) {
     89             if (sProfileMap.size() == 0) {
     90                 LocalBluetoothProfileManager profileManager;
     91 
     92                 profileManager = new A2dpProfileManager(localManager);
     93                 sProfileMap.put(Profile.A2DP, profileManager);
     94 
     95                 profileManager = new HeadsetProfileManager(localManager);
     96                 sProfileMap.put(Profile.HEADSET, profileManager);
     97 
     98                 profileManager = new OppProfileManager(localManager);
     99                 sProfileMap.put(Profile.OPP, profileManager);
    100             }
    101         }
    102     }
    103 
    104     private static LinkedList<ServiceListener> mServiceListeners = new LinkedList<ServiceListener>();
    105 
    106     public static void addServiceListener(ServiceListener l) {
    107         mServiceListeners.add(l);
    108     }
    109 
    110     public static void removeServiceListener(ServiceListener l) {
    111         mServiceListeners.remove(l);
    112     }
    113 
    114     public static boolean isManagerReady() {
    115         // Getting just the headset profile is fine for now. Will need to deal with A2DP
    116         // and others if they aren't always in a ready state.
    117         LocalBluetoothProfileManager profileManager = sProfileMap.get(Profile.HEADSET);
    118         if (profileManager == null) {
    119             return sProfileMap.size() > 0;
    120         }
    121         return profileManager.isProfileReady();
    122     }
    123 
    124     public static LocalBluetoothProfileManager getProfileManager(LocalBluetoothManager localManager,
    125             Profile profile) {
    126         // Note: This code assumes that "localManager" is same as the
    127         // LocalBluetoothManager that was used to initialize the sProfileMap.
    128         // If that every changes, we can't just keep one copy of sProfileMap.
    129         synchronized (sProfileMap) {
    130             LocalBluetoothProfileManager profileManager = sProfileMap.get(profile);
    131             if (profileManager == null) {
    132                 Log.e(TAG, "profileManager can't be found for " + profile.toString());
    133             }
    134             return profileManager;
    135         }
    136     }
    137 
    138     /**
    139      * Temporary method to fill profiles based on a device's class.
    140      *
    141      * NOTE: This list happens to define the connection order. We should put this logic in a more
    142      * well known place when this method is no longer temporary.
    143      * @param uuids of the remote device
    144      * @param profiles The list of profiles to fill
    145      */
    146     public static void updateProfiles(ParcelUuid[] uuids, List<Profile> profiles) {
    147         profiles.clear();
    148 
    149         if (uuids == null) {
    150             return;
    151         }
    152 
    153         if (BluetoothUuid.containsAnyUuid(uuids, HEADSET_PROFILE_UUIDS)) {
    154             profiles.add(Profile.HEADSET);
    155         }
    156 
    157         if (BluetoothUuid.containsAnyUuid(uuids, A2DP_PROFILE_UUIDS)) {
    158             profiles.add(Profile.A2DP);
    159         }
    160 
    161         if (BluetoothUuid.containsAnyUuid(uuids, OPP_PROFILE_UUIDS)) {
    162             profiles.add(Profile.OPP);
    163         }
    164     }
    165 
    166     protected LocalBluetoothProfileManager(LocalBluetoothManager localManager) {
    167         mLocalManager = localManager;
    168     }
    169 
    170     public abstract Set<BluetoothDevice> getConnectedDevices();
    171 
    172     public abstract boolean connect(BluetoothDevice device);
    173 
    174     public abstract boolean disconnect(BluetoothDevice device);
    175 
    176     public abstract int getConnectionStatus(BluetoothDevice device);
    177 
    178     public abstract int getSummary(BluetoothDevice device);
    179 
    180     public abstract int convertState(int a2dpState);
    181 
    182     public abstract boolean isPreferred(BluetoothDevice device);
    183 
    184     public abstract int getPreferred(BluetoothDevice device);
    185 
    186     public abstract void setPreferred(BluetoothDevice device, boolean preferred);
    187 
    188     public boolean isConnected(BluetoothDevice device) {
    189         return SettingsBtStatus.isConnectionStatusConnected(getConnectionStatus(device));
    190     }
    191 
    192     public abstract boolean isProfileReady();
    193 
    194     // TODO: int instead of enum
    195     public enum Profile {
    196         HEADSET(R.string.bluetooth_profile_headset),
    197         A2DP(R.string.bluetooth_profile_a2dp),
    198         OPP(R.string.bluetooth_profile_opp);
    199 
    200         public final int localizedString;
    201 
    202         private Profile(int localizedString) {
    203             this.localizedString = localizedString;
    204         }
    205     }
    206 
    207     /**
    208      * A2dpProfileManager is an abstraction for the {@link BluetoothA2dp} service.
    209      */
    210     private static class A2dpProfileManager extends LocalBluetoothProfileManager {
    211         private BluetoothA2dp mService;
    212 
    213         public A2dpProfileManager(LocalBluetoothManager localManager) {
    214             super(localManager);
    215             mService = new BluetoothA2dp(localManager.getContext());
    216         }
    217 
    218         @Override
    219         public Set<BluetoothDevice> getConnectedDevices() {
    220             return mService.getNonDisconnectedSinks();
    221         }
    222 
    223         @Override
    224         public boolean connect(BluetoothDevice device) {
    225             Set<BluetoothDevice> sinks = mService.getNonDisconnectedSinks();
    226             if (sinks != null) {
    227                 for (BluetoothDevice sink : sinks) {
    228                     mService.disconnectSink(sink);
    229                 }
    230             }
    231             return mService.connectSink(device);
    232         }
    233 
    234         @Override
    235         public boolean disconnect(BluetoothDevice device) {
    236             // Downgrade priority as user is disconnecting the sink.
    237             if (mService.getSinkPriority(device) > BluetoothA2dp.PRIORITY_ON) {
    238                 mService.setSinkPriority(device, BluetoothA2dp.PRIORITY_ON);
    239             }
    240             return mService.disconnectSink(device);
    241         }
    242 
    243         @Override
    244         public int getConnectionStatus(BluetoothDevice device) {
    245             return convertState(mService.getSinkState(device));
    246         }
    247 
    248         @Override
    249         public int getSummary(BluetoothDevice device) {
    250             int connectionStatus = getConnectionStatus(device);
    251 
    252             if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) {
    253                 return R.string.bluetooth_a2dp_profile_summary_connected;
    254             } else {
    255                 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
    256             }
    257         }
    258 
    259         @Override
    260         public boolean isPreferred(BluetoothDevice device) {
    261             return mService.getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF;
    262         }
    263 
    264         @Override
    265         public int getPreferred(BluetoothDevice device) {
    266             return mService.getSinkPriority(device);
    267         }
    268 
    269         @Override
    270         public void setPreferred(BluetoothDevice device, boolean preferred) {
    271             if (preferred) {
    272                 if (mService.getSinkPriority(device) < BluetoothA2dp.PRIORITY_ON) {
    273                     mService.setSinkPriority(device, BluetoothA2dp.PRIORITY_ON);
    274                 }
    275             } else {
    276                 mService.setSinkPriority(device, BluetoothA2dp.PRIORITY_OFF);
    277             }
    278         }
    279 
    280         @Override
    281         public int convertState(int a2dpState) {
    282             switch (a2dpState) {
    283             case BluetoothA2dp.STATE_CONNECTED:
    284                 return SettingsBtStatus.CONNECTION_STATUS_CONNECTED;
    285             case BluetoothA2dp.STATE_CONNECTING:
    286                 return SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    287             case BluetoothA2dp.STATE_DISCONNECTED:
    288                 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED;
    289             case BluetoothA2dp.STATE_DISCONNECTING:
    290                 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING;
    291             case BluetoothA2dp.STATE_PLAYING:
    292                 return SettingsBtStatus.CONNECTION_STATUS_ACTIVE;
    293             default:
    294                 return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN;
    295             }
    296         }
    297 
    298         @Override
    299         public boolean isProfileReady() {
    300             return true;
    301         }
    302     }
    303 
    304     /**
    305      * HeadsetProfileManager is an abstraction for the {@link BluetoothHeadset} service.
    306      */
    307     private static class HeadsetProfileManager extends LocalBluetoothProfileManager
    308             implements BluetoothHeadset.ServiceListener {
    309         private BluetoothHeadset mService;
    310         private Handler mUiHandler = new Handler();
    311         private boolean profileReady = false;
    312         private BluetoothDevice mDelayedConnectDevice = null;
    313         private BluetoothDevice mDelayedDisconnectDevice = null;
    314 
    315         public HeadsetProfileManager(LocalBluetoothManager localManager) {
    316             super(localManager);
    317             mService = new BluetoothHeadset(localManager.getContext(), this);
    318         }
    319 
    320         public void onServiceConnected() {
    321             profileReady = true;
    322             // This could be called on a non-UI thread, funnel to UI thread.
    323             // Delay for a few seconds to allow other proxies to connect.
    324             mUiHandler.postDelayed(new Runnable() {
    325                 public void run() {
    326                     BluetoothDevice device = mService.getCurrentHeadset();
    327 
    328                     if (mDelayedConnectDevice != null) {
    329                         Log.i(TAG, "service ready: connecting...");
    330                         BluetoothDevice newDevice = mDelayedConnectDevice;
    331                         mDelayedConnectDevice = null;
    332 
    333                         if (!newDevice.equals(device)) {
    334                             if (device != null) {
    335                                 Log.i(TAG, "disconnecting old headset");
    336                                 mService.disconnectHeadset(device);
    337                             }
    338                             Log.i(TAG, "connecting to pending headset");
    339                             mService.connectHeadset(newDevice);
    340                         }
    341                     } else if (mDelayedDisconnectDevice != null) {
    342                         Log.i(TAG, "service ready: disconnecting...");
    343                         if (mDelayedDisconnectDevice.equals(device)) {
    344                             Log.i(TAG, "disconnecting headset");
    345                             mService.disconnectHeadset(device);
    346                         }
    347                         mDelayedDisconnectDevice = null;
    348                     } else {
    349                         /*
    350                          * We just bound to the service, so refresh the UI of the
    351                          * headset device.
    352                          */
    353                         if (device == null) return;
    354                         mLocalManager.getCachedDeviceManager()
    355                             .onProfileStateChanged(device, Profile.HEADSET,
    356                                                    BluetoothHeadset.STATE_CONNECTED);
    357                     }
    358                 }
    359             }, 2000);  // wait 2 seconds for other proxies to connect
    360 
    361             if (mServiceListeners.size() > 0) {
    362                 Iterator<ServiceListener> it = mServiceListeners.iterator();
    363                 while(it.hasNext()) {
    364                     it.next().onServiceConnected();
    365                 }
    366             }
    367         }
    368 
    369         public void onServiceDisconnected() {
    370             profileReady = false;
    371             if (mServiceListeners.size() > 0) {
    372                 Iterator<ServiceListener> it = mServiceListeners.iterator();
    373                 while(it.hasNext()) {
    374                     it.next().onServiceDisconnected();
    375                 }
    376             }
    377         }
    378 
    379         @Override
    380         public boolean isProfileReady() {
    381             return profileReady;
    382         }
    383 
    384         @Override
    385         public Set<BluetoothDevice> getConnectedDevices() {
    386             Set<BluetoothDevice> devices = null;
    387             BluetoothDevice device = mService.getCurrentHeadset();
    388             if (device != null) {
    389                 devices = new HashSet<BluetoothDevice>();
    390                 devices.add(device);
    391             }
    392             return devices;
    393         }
    394 
    395         @Override
    396         public boolean connect(BluetoothDevice device) {
    397             // Delay connection until onServiceConnected() if the
    398             // manager isn't ready
    399             if (!isManagerReady()) {
    400                 Log.w(TAG, "HeadsetProfileManager delaying connect, "
    401                         + "manager not ready");
    402                 mDelayedConnectDevice = device;
    403                 mDelayedDisconnectDevice = null;
    404                 return true;  // hopefully it will succeed
    405             }
    406 
    407             // Since connectHeadset fails if already connected to a headset, we
    408             // disconnect from any headset first
    409             BluetoothDevice currDevice = mService.getCurrentHeadset();
    410             if (currDevice != null) {
    411                 mService.disconnectHeadset(currDevice);
    412             }
    413             return mService.connectHeadset(device);
    414         }
    415 
    416         @Override
    417         public boolean disconnect(BluetoothDevice device) {
    418             // Delay connection until onServiceConnected() if the
    419             // manager isn't ready
    420             if (!isManagerReady()) {
    421                 Log.w(TAG, "HeadsetProfileManager delaying disconnect, "
    422                         + "manager not ready");
    423                 mDelayedConnectDevice = null;
    424                 mDelayedDisconnectDevice = device;
    425                 return true;  // hopefully it will succeed
    426             }
    427 
    428             BluetoothDevice currDevice = mService.getCurrentHeadset();
    429             if (currDevice != null && currDevice.equals(device)) {
    430                 // Downgrade prority as user is disconnecting the headset.
    431                 if (mService.getPriority(device) > BluetoothHeadset.PRIORITY_ON) {
    432                     mService.setPriority(device, BluetoothHeadset.PRIORITY_ON);
    433                 }
    434                 return mService.disconnectHeadset(device);
    435             } else {
    436                 return false;
    437             }
    438         }
    439 
    440         @Override
    441         public int getConnectionStatus(BluetoothDevice device) {
    442             BluetoothDevice currentDevice = mService.getCurrentHeadset();
    443             return currentDevice != null && currentDevice.equals(device)
    444                     ? convertState(mService.getState(device))
    445                     : SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED;
    446         }
    447 
    448         @Override
    449         public int getSummary(BluetoothDevice device) {
    450             int connectionStatus = getConnectionStatus(device);
    451 
    452             if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) {
    453                 return R.string.bluetooth_headset_profile_summary_connected;
    454             } else {
    455                 return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
    456             }
    457         }
    458 
    459         @Override
    460         public boolean isPreferred(BluetoothDevice device) {
    461             return mService.getPriority(device) > BluetoothHeadset.PRIORITY_OFF;
    462         }
    463 
    464         @Override
    465         public int getPreferred(BluetoothDevice device) {
    466             return mService.getPriority(device);
    467         }
    468 
    469         @Override
    470         public void setPreferred(BluetoothDevice device, boolean preferred) {
    471             if (preferred) {
    472                 if (mService.getPriority(device) < BluetoothHeadset.PRIORITY_ON) {
    473                     mService.setPriority(device, BluetoothHeadset.PRIORITY_ON);
    474                 }
    475             } else {
    476                 mService.setPriority(device, BluetoothHeadset.PRIORITY_OFF);
    477             }
    478         }
    479 
    480         @Override
    481         public int convertState(int headsetState) {
    482             switch (headsetState) {
    483             case BluetoothHeadset.STATE_CONNECTED:
    484                 return SettingsBtStatus.CONNECTION_STATUS_CONNECTED;
    485             case BluetoothHeadset.STATE_CONNECTING:
    486                 return SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    487             case BluetoothHeadset.STATE_DISCONNECTED:
    488                 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED;
    489             default:
    490                 return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN;
    491             }
    492         }
    493     }
    494 
    495     /**
    496      * OppProfileManager
    497      */
    498     private static class OppProfileManager extends LocalBluetoothProfileManager {
    499 
    500         public OppProfileManager(LocalBluetoothManager localManager) {
    501             super(localManager);
    502         }
    503 
    504         @Override
    505         public Set<BluetoothDevice> getConnectedDevices() {
    506             return null;
    507         }
    508 
    509         @Override
    510         public boolean connect(BluetoothDevice device) {
    511             return false;
    512         }
    513 
    514         @Override
    515         public boolean disconnect(BluetoothDevice device) {
    516             return false;
    517         }
    518 
    519         @Override
    520         public int getConnectionStatus(BluetoothDevice device) {
    521             return -1;
    522         }
    523 
    524         @Override
    525         public int getSummary(BluetoothDevice device) {
    526             int connectionStatus = getConnectionStatus(device);
    527 
    528             if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) {
    529                 return R.string.bluetooth_opp_profile_summary_connected;
    530             } else {
    531                 return R.string.bluetooth_opp_profile_summary_not_connected;
    532             }
    533         }
    534 
    535         @Override
    536         public boolean isPreferred(BluetoothDevice device) {
    537             return false;
    538         }
    539 
    540         @Override
    541         public int getPreferred(BluetoothDevice device) {
    542             return -1;
    543         }
    544 
    545         @Override
    546         public void setPreferred(BluetoothDevice device, boolean preferred) {
    547         }
    548 
    549         @Override
    550         public boolean isProfileReady() {
    551             return true;
    552         }
    553 
    554         @Override
    555         public int convertState(int oppState) {
    556             switch (oppState) {
    557             case 0:
    558                 return SettingsBtStatus.CONNECTION_STATUS_CONNECTED;
    559             case 1:
    560                 return SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
    561             case 2:
    562                 return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED;
    563             default:
    564                 return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN;
    565             }
    566         }
    567     }
    568 }
    569