Home | History | Annotate | Download | only in connserv
      1 /*
      2  * Copyright (C) 2016 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 package com.android.bluetooth.hfpclient.connserv;
     17 
     18 import android.bluetooth.BluetoothAdapter;
     19 import android.bluetooth.BluetoothDevice;
     20 import android.bluetooth.BluetoothHeadsetClient;
     21 import android.bluetooth.BluetoothHeadsetClientCall;
     22 import android.bluetooth.BluetoothProfile;
     23 import android.content.BroadcastReceiver;
     24 import android.content.ComponentName;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.net.Uri;
     29 import android.os.Bundle;
     30 import android.os.Handler;
     31 import android.telecom.Connection;
     32 import android.telecom.ConnectionRequest;
     33 import android.telecom.ConnectionService;
     34 import android.telecom.PhoneAccount;
     35 import android.telecom.PhoneAccountHandle;
     36 import android.telecom.TelecomManager;
     37 import android.util.Log;
     38 
     39 import com.android.bluetooth.hfpclient.HeadsetClientService;
     40 
     41 import java.util.Arrays;
     42 import java.util.ArrayList;
     43 import java.util.Collection;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Map;
     47 
     48 public class HfpClientConnectionService extends ConnectionService {
     49     private static final String TAG = "HfpClientConnService";
     50 
     51     public static final String HFP_SCHEME = "hfpc";
     52 
     53     private BluetoothAdapter mAdapter;
     54     // Currently active device.
     55     private BluetoothDevice mDevice;
     56     // Phone account associated with the above device.
     57     private PhoneAccount mDevicePhoneAccount;
     58     // BluetoothHeadset proxy.
     59     private BluetoothHeadsetClient mHeadsetProfile;
     60     private TelecomManager mTelecomManager;
     61 
     62     private Map<Uri, HfpClientConnection> mConnections = new HashMap<>();
     63     private HfpClientConference mConference;
     64 
     65     private boolean mPendingAcceptCall;
     66 
     67     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
     68         @Override
     69         public void onReceive(Context context, Intent intent) {
     70             Log.d(TAG, "onReceive " + intent);
     71             String action = intent != null ? intent.getAction() : null;
     72 
     73             if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
     74                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
     75                 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
     76 
     77                 if (newState == BluetoothProfile.STATE_CONNECTED) {
     78                     Log.d(TAG, "Established connection with " + device);
     79                     synchronized (HfpClientConnectionService.this) {
     80                         if (device.equals(mDevice)) {
     81                             // We are already connected and this message can be safeuly ignored.
     82                             Log.w(TAG, "Got connected for previously connected device, ignoring.");
     83                         } else {
     84                             // Since we are connected to a new device close down the previous
     85                             // account and register the new one.
     86                             if (mDevicePhoneAccount != null) {
     87                                 mTelecomManager.unregisterPhoneAccount(
     88                                     mDevicePhoneAccount.getAccountHandle());
     89                             }
     90                             // Reset the device and the phone account associated.
     91                             mDevice = device;
     92                             mDevicePhoneAccount =
     93                                 getAccount(HfpClientConnectionService.this, device);
     94                             mTelecomManager.registerPhoneAccount(mDevicePhoneAccount);
     95                             mTelecomManager.enablePhoneAccount(
     96                                 mDevicePhoneAccount.getAccountHandle(), true);
     97                             mTelecomManager.setUserSelectedOutgoingPhoneAccount(
     98                                 mDevicePhoneAccount.getAccountHandle());
     99                         }
    100                     }
    101 
    102                     // Add any existing calls to the telecom stack.
    103                     if (mHeadsetProfile != null) {
    104                         List<BluetoothHeadsetClientCall> calls =
    105                                 mHeadsetProfile.getCurrentCalls(mDevice);
    106                         Log.d(TAG, "Got calls " + calls);
    107                         for (BluetoothHeadsetClientCall call : calls) {
    108                             handleCall(call);
    109                         }
    110                     } else {
    111                     }
    112                 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
    113                     Log.d(TAG, "Disconnecting from " + device);
    114                     // Disconnect any inflight calls from the connection service.
    115                     synchronized (HfpClientConnectionService.this) {
    116                         if (device.equals(mDevice)) {
    117                             Log.d(TAG, "Resetting state for " + device);
    118                             mDevice = null;
    119                             disconnectAll();
    120                             mTelecomManager.unregisterPhoneAccount(
    121                                 mDevicePhoneAccount.getAccountHandle());
    122                             mDevicePhoneAccount = null;
    123                         }
    124                     }
    125                 }
    126             } else if (BluetoothHeadsetClient.ACTION_CALL_CHANGED.equals(action)) {
    127                 // If we are not connected, then when we actually do get connected -- the calls should
    128                 // be added (see ACTION_CONNECTION_STATE_CHANGED intent above).
    129                 handleCall((BluetoothHeadsetClientCall)
    130                         intent.getParcelableExtra(BluetoothHeadsetClient.EXTRA_CALL));
    131                 Log.d(TAG, mConnections.size() + " remaining");
    132             }
    133         }
    134     };
    135 
    136     @Override
    137     public void onCreate() {
    138         super.onCreate();
    139         Log.d(TAG, "onCreate");
    140         mAdapter = BluetoothAdapter.getDefaultAdapter();
    141         mTelecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
    142         mAdapter.getProfileProxy(this, mServiceListener, BluetoothProfile.HEADSET_CLIENT);
    143     }
    144 
    145     @Override
    146     public void onDestroy() {
    147         Log.d(TAG, "onDestroy called");
    148         // Close the profile.
    149         if (mHeadsetProfile != null) {
    150             mAdapter.closeProfileProxy(BluetoothProfile.HEADSET_CLIENT, mHeadsetProfile);
    151         }
    152 
    153         // Unregister the broadcast receiver.
    154         try {
    155             unregisterReceiver(mBroadcastReceiver);
    156         } catch (IllegalArgumentException ex) {
    157             Log.w(TAG, "Receiver was not registered.");
    158         }
    159 
    160         // Unregister the phone account. This should ideally happen when disconnection ensues but in
    161         // case the service crashes we may need to force clean.
    162         synchronized (this) {
    163             mDevice = null;
    164             if (mDevicePhoneAccount != null) {
    165                 mTelecomManager.unregisterPhoneAccount(mDevicePhoneAccount.getAccountHandle());
    166                 mDevicePhoneAccount = null;
    167             }
    168         }
    169     }
    170 
    171     @Override
    172     public int onStartCommand(Intent intent, int flags, int startId) {
    173         Log.d(TAG, "onStartCommand " + intent);
    174         // In order to make sure that the service is sticky (recovers from errors when HFP
    175         // connection is still active) and to stop it we need a special intent since stopService
    176         // only recreates it.
    177         if (intent.getBooleanExtra(HeadsetClientService.HFP_CLIENT_STOP_TAG, false)) {
    178             // Stop the service.
    179             stopSelf();
    180             return 0;
    181         } else {
    182             IntentFilter filter = new IntentFilter();
    183             filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
    184             filter.addAction(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
    185             registerReceiver(mBroadcastReceiver, filter);
    186             return START_STICKY;
    187         }
    188     }
    189 
    190     private void handleCall(BluetoothHeadsetClientCall call) {
    191         Log.d(TAG, "Got call " + call);
    192 
    193         Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
    194         HfpClientConnection connection = mConnections.get(number);
    195         if (connection != null) {
    196             connection.handleCallChanged(call);
    197         }
    198 
    199         if (connection == null) {
    200             // Create the connection here, trigger Telecom to bind to us.
    201             buildConnection(call.getDevice(), call, number);
    202 
    203             PhoneAccountHandle handle = getHandle();
    204             TelecomManager manager =
    205                     (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
    206 
    207             // Depending on where this call originated make it an incoming call or outgoing
    208             // (represented as unknown call in telecom since). Since BluetoothHeadsetClientCall is a
    209             // parcelable we simply pack the entire object in there.
    210             Bundle b = new Bundle();
    211             if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING ||
    212                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING ||
    213                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE) {
    214                 // This is an outgoing call. Even if it is an active call we do not have a way of
    215                 // putting that parcelable in a seaprate field.
    216                 b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call);
    217                 manager.addNewUnknownCall(handle, b);
    218             } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING) {
    219                 // This is an incoming call.
    220                 b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call);
    221                 manager.addNewIncomingCall(handle, b);
    222             }
    223         } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
    224             Log.d(TAG, "Removing number " + number);
    225             mConnections.remove(number);
    226         }
    227         updateConferenceableConnections();
    228     }
    229 
    230     // This method is called whenever there is a new incoming call (or right after BT connection).
    231     @Override
    232     public Connection onCreateIncomingConnection(
    233             PhoneAccountHandle connectionManagerAccount,
    234             ConnectionRequest request) {
    235         Log.d(TAG, "onCreateIncomingConnection " + connectionManagerAccount + " req: " + request);
    236         if (connectionManagerAccount != null &&
    237                 !getHandle().equals(connectionManagerAccount)) {
    238             Log.w(TAG, "HfpClient does not support having a connection manager");
    239             return null;
    240         }
    241 
    242         // We should already have a connection by this time.
    243         BluetoothHeadsetClientCall call =
    244             request.getExtras().getParcelable(
    245                 TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
    246         Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
    247         HfpClientConnection connection = mConnections.get(number);
    248 
    249         if (connection != null) {
    250             connection.onAdded();
    251             updateConferenceableConnections();
    252             return connection;
    253         } else {
    254             Log.e(TAG, "Connection should exist in our db, if it doesn't we dont know how to " +
    255                 "handle this call.");
    256             return null;
    257         }
    258     }
    259 
    260     // This method is called *only if* Dialer UI is used to place an outgoing call.
    261     @Override
    262     public Connection onCreateOutgoingConnection(
    263             PhoneAccountHandle connectionManagerAccount,
    264             ConnectionRequest request) {
    265         Log.d(TAG, "onCreateOutgoingConnection " + connectionManagerAccount);
    266         if (connectionManagerAccount != null &&
    267                 !getHandle().equals(connectionManagerAccount)) {
    268             Log.w(TAG, "HfpClient does not support having a connection manager");
    269             return null;
    270         }
    271 
    272         HfpClientConnection connection =
    273                 buildConnection(getDevice(request.getAccountHandle()), null, request.getAddress());
    274         connection.onAdded();
    275         return connection;
    276     }
    277 
    278     // This method is called when:
    279     // 1. Outgoing call created from the AG.
    280     // 2. Call transfer from AG -> HF (on connection when existed call present).
    281     @Override
    282     public Connection onCreateUnknownConnection(
    283             PhoneAccountHandle connectionManagerAccount,
    284             ConnectionRequest request) {
    285         Log.d(TAG, "onCreateUnknownConnection " + connectionManagerAccount);
    286         if (connectionManagerAccount != null &&
    287                 !getHandle().equals(connectionManagerAccount)) {
    288             Log.w(TAG, "HfpClient does not support having a connection manager");
    289             return null;
    290         }
    291 
    292         // We should already have a connection by this time.
    293         BluetoothHeadsetClientCall call =
    294             request.getExtras().getParcelable(
    295                 TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
    296         Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
    297         HfpClientConnection connection = mConnections.get(number);
    298 
    299         if (connection != null) {
    300             connection.onAdded();
    301             updateConferenceableConnections();
    302             return connection;
    303         } else {
    304             Log.e(TAG, "Connection should exist in our db, if it doesn't we dont know how to " +
    305                 "handle this call " + call);
    306             return null;
    307         }
    308     }
    309 
    310     @Override
    311     public void onConference(Connection connection1, Connection connection2) {
    312         Log.d(TAG, "onConference " + connection1 + " " + connection2);
    313         if (mConference == null) {
    314             BluetoothDevice device = getDevice(getHandle());
    315             mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile);
    316             addConference(mConference);
    317         }
    318         mConference.setActive();
    319         if (connection1.getConference() == null) {
    320             mConference.addConnection(connection1);
    321         }
    322         if (connection2.getConference() == null) {
    323             mConference.addConnection(connection2);
    324         }
    325     }
    326 
    327     private void updateConferenceableConnections() {
    328         Collection<HfpClientConnection> all = mConnections.values();
    329 
    330         List<Connection> held = new ArrayList<>();
    331         List<Connection> active = new ArrayList<>();
    332         List<Connection> group = new ArrayList<>();
    333         for (HfpClientConnection connection : all) {
    334             switch (connection.getState()) {
    335                 case Connection.STATE_ACTIVE:
    336                     active.add(connection);
    337                     break;
    338                 case Connection.STATE_HOLDING:
    339                     held.add(connection);
    340                     break;
    341                 default:
    342                     break;
    343             }
    344             if (connection.inConference()) {
    345                 group.add(connection);
    346             }
    347         }
    348         for (Connection connection : held) {
    349             connection.setConferenceableConnections(active);
    350         }
    351         for (Connection connection : active) {
    352             connection.setConferenceableConnections(held);
    353         }
    354         if (group.size() > 1 && mConference == null) {
    355             BluetoothDevice device = getDevice(getHandle());
    356             mConference = new HfpClientConference(getHandle(), device, mHeadsetProfile);
    357             if (group.get(0).getState() == Connection.STATE_ACTIVE) {
    358                 mConference.setActive();
    359             } else {
    360                 mConference.setOnHold();
    361             }
    362             for (Connection connection : group) {
    363                 mConference.addConnection(connection);
    364             }
    365             addConference(mConference);
    366         }
    367         if (mConference != null) {
    368             List<Connection> toRemove = new ArrayList<>();
    369             for (Connection connection : mConference.getConnections()) {
    370                 if (!((HfpClientConnection) connection).inConference()) {
    371                     toRemove.add(connection);
    372                 }
    373             }
    374             for (Connection connection : toRemove) {
    375                 mConference.removeConnection(connection);
    376             }
    377             if (mConference.getConnections().size() <= 1) {
    378                 mConference.destroy();
    379                 mConference = null;
    380             } else {
    381                 List<Connection> notConferenced = new ArrayList<>();
    382                 for (Connection connection : all) {
    383                     if (connection.getConference() == null &&
    384                             (connection.getState() == Connection.STATE_HOLDING ||
    385                              connection.getState() == Connection.STATE_ACTIVE)) {
    386                         if (((HfpClientConnection) connection).inConference()) {
    387                             mConference.addConnection(connection);
    388                         } else {
    389                             notConferenced.add(connection);
    390                         }
    391                     }
    392                 }
    393                 mConference.setConferenceableConnections(notConferenced);
    394             }
    395         }
    396     }
    397 
    398     private void disconnectAll() {
    399         for (HfpClientConnection connection : mConnections.values()) {
    400             connection.onHfpDisconnected();
    401         }
    402         if (mConference != null) {
    403             mConference.destroy();
    404             mConference = null;
    405         }
    406     }
    407 
    408     private BluetoothDevice getDevice(PhoneAccountHandle handle) {
    409         PhoneAccount account = mTelecomManager.getPhoneAccount(handle);
    410         String btAddr = account.getAddress().getSchemeSpecificPart();
    411         return mAdapter.getRemoteDevice(btAddr);
    412     }
    413 
    414     private HfpClientConnection buildConnection(
    415             BluetoothDevice device, BluetoothHeadsetClientCall call, Uri number) {
    416         Log.d(TAG, "Creating connection on " + device + " for " + call + "/" + number);
    417         HfpClientConnection connection =
    418                 new HfpClientConnection(this, device, mHeadsetProfile, call, number);
    419         mConnections.put(number, connection);
    420         return connection;
    421     }
    422 
    423     BluetoothProfile.ServiceListener mServiceListener = new BluetoothProfile.ServiceListener() {
    424         @Override
    425         public void onServiceConnected(int profile, BluetoothProfile proxy) {
    426             Log.d(TAG, "onServiceConnected");
    427             mHeadsetProfile = (BluetoothHeadsetClient) proxy;
    428 
    429             List<BluetoothDevice> devices = mHeadsetProfile.getConnectedDevices();
    430             if (devices == null || devices.size() != 1) {
    431                 Log.w(TAG, "No connected or more than one connected devices found." + devices);
    432             } else { // We have exactly one device connected.
    433                 Log.d(TAG, "Creating phone account.");
    434                 synchronized (HfpClientConnectionService.this) {
    435                     mDevice = devices.get(0);
    436                     mDevicePhoneAccount = getAccount(HfpClientConnectionService.this, mDevice);
    437                     mTelecomManager.registerPhoneAccount(mDevicePhoneAccount);
    438                     mTelecomManager.enablePhoneAccount(
    439                         mDevicePhoneAccount.getAccountHandle(), true);
    440                     mTelecomManager.setUserSelectedOutgoingPhoneAccount(
    441                         mDevicePhoneAccount.getAccountHandle());
    442                 }
    443             }
    444 
    445             for (HfpClientConnection connection : mConnections.values()) {
    446                 connection.onHfpConnected(mHeadsetProfile);
    447             }
    448 
    449             List<BluetoothHeadsetClientCall> calls = mHeadsetProfile.getCurrentCalls(mDevice);
    450             Log.d(TAG, "Got calls " + calls);
    451             if (calls != null) {
    452                 for (BluetoothHeadsetClientCall call : calls) {
    453                     handleCall(call);
    454                 }
    455             }
    456 
    457             if (mPendingAcceptCall) {
    458                 mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
    459                 mPendingAcceptCall = false;
    460             }
    461         }
    462 
    463         @Override
    464         public void onServiceDisconnected(int profile) {
    465             Log.d(TAG, "onServiceDisconnected " + profile);
    466             mHeadsetProfile = null;
    467             disconnectAll();
    468         }
    469     };
    470 
    471     public static boolean hasHfpClientEcc(BluetoothHeadsetClient client, BluetoothDevice device) {
    472         Bundle features = client.getCurrentAgEvents(device);
    473         return features == null ? false :
    474                 features.getBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_ECC, false);
    475     }
    476 
    477     public synchronized PhoneAccountHandle getHandle() {
    478         if (mDevicePhoneAccount == null) throw new IllegalStateException("Handle null??");
    479         return mDevicePhoneAccount.getAccountHandle();
    480     }
    481 
    482     public static PhoneAccount getAccount(Context context, BluetoothDevice device) {
    483         Uri addr = Uri.fromParts(HfpClientConnectionService.HFP_SCHEME, device.getAddress(), null);
    484         PhoneAccountHandle handle = new PhoneAccountHandle(
    485             new ComponentName(context, HfpClientConnectionService.class), device.getAddress());
    486         PhoneAccount account =
    487                 new PhoneAccount.Builder(handle, "HFP")
    488                     .setAddress(addr)
    489                     .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
    490                     .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
    491                     .build();
    492         Log.d(TAG, "phoneaccount: " + account);
    493         return account;
    494     }
    495 }
    496