Home | History | Annotate | Download | only in connserv
      1 /*
      2  * Copyright (C) 2017 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.DisconnectCause;
     35 import android.telecom.PhoneAccount;
     36 import android.telecom.PhoneAccountHandle;
     37 import android.telecom.TelecomManager;
     38 import android.util.Log;
     39 
     40 import com.android.bluetooth.hfpclient.HeadsetClientService;
     41 
     42 import java.util.Arrays;
     43 import java.util.ArrayList;
     44 import java.util.Collection;
     45 import java.util.HashMap;
     46 import java.util.List;
     47 import java.util.Map;
     48 import java.util.UUID;
     49 
     50 // Helper class that manages the call handling for one device. HfpClientConnectionService holdes a
     51 // list of such blocks and routes traffic from the UI.
     52 //
     53 // Lifecycle of a Device Block is managed entirely by the Service which creates it. In essence it
     54 // has only the active state otherwise the block should be GCed.
     55 public class HfpClientDeviceBlock {
     56     private final String TAG;
     57     private final boolean DBG = false;
     58     private final Context mContext;
     59     private final BluetoothDevice mDevice;
     60     private final PhoneAccount mPhoneAccount;
     61     private final Map<UUID, HfpClientConnection> mConnections = new HashMap<>();
     62     private final TelecomManager mTelecomManager;
     63     private final HfpClientConnectionService mConnServ;
     64     private HfpClientConference mConference;
     65 
     66     private BluetoothHeadsetClient mHeadsetProfile;
     67 
     68     HfpClientDeviceBlock(
     69             HfpClientConnectionService connServ,
     70             BluetoothDevice device,
     71             BluetoothHeadsetClient headsetProfile) {
     72         mConnServ = connServ;
     73         mContext = connServ;
     74         mDevice = device;
     75         TAG = "HfpClientDeviceBlock." + mDevice.getAddress();
     76         mPhoneAccount = HfpClientConnectionService.createAccount(mContext, device);
     77         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
     78 
     79         // Register the phone account since block is created only when devices are connected
     80         mTelecomManager.registerPhoneAccount(mPhoneAccount);
     81         mTelecomManager.enablePhoneAccount(mPhoneAccount.getAccountHandle(), true);
     82         mTelecomManager.setUserSelectedOutgoingPhoneAccount(mPhoneAccount.getAccountHandle());
     83         mHeadsetProfile = headsetProfile;
     84 
     85         // Read the current calls and add them to telecom if already present
     86         if (mHeadsetProfile != null) {
     87             List<BluetoothHeadsetClientCall> calls =
     88                     mHeadsetProfile.getCurrentCalls(mDevice);
     89             if (DBG) {
     90                 Log.d(TAG, "Got calls " + calls);
     91             }
     92             if (calls == null) {
     93                 // We can get null as a return if we are not connected. Hence there may
     94                 // be a race in getting the broadcast and HFP Client getting
     95                 // disconnected before broadcast gets delivered.
     96                 Log.w(TAG, "Got connected but calls were null, ignoring the broadcast");
     97                 return;
     98             }
     99 
    100             for (BluetoothHeadsetClientCall call : calls) {
    101                 handleCall(call);
    102             }
    103         } else {
    104             Log.e(TAG, "headset profile is null, ignoring broadcast.");
    105         }
    106     }
    107 
    108     synchronized Connection onCreateIncomingConnection(BluetoothHeadsetClientCall call) {
    109         HfpClientConnection connection = connection = mConnections.get(call.getUUID());
    110         if (connection != null) {
    111             connection.onAdded();
    112             updateConferenceableConnections();
    113             return connection;
    114         } else {
    115             Log.e(TAG, "Call " + call + " ignored: connection does not exist");
    116             return null;
    117         }
    118     }
    119 
    120     Connection onCreateOutgoingConnection(Uri address) {
    121         HfpClientConnection connection = buildConnection(null, address);
    122         if (connection != null) {
    123             connection.onAdded();
    124         }
    125         return connection;
    126     }
    127 
    128     synchronized Connection onCreateUnknownConnection(BluetoothHeadsetClientCall call) {
    129         Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null);
    130         HfpClientConnection connection = connection = mConnections.get(call.getUUID());
    131 
    132         if (connection != null) {
    133             connection.onAdded();
    134             updateConferenceableConnections();
    135             return connection;
    136         } else {
    137             Log.e(TAG, "Call " + call + " ignored: connection does not exist");
    138             return null;
    139         }
    140     }
    141 
    142     synchronized void onConference(Connection connection1, Connection connection2) {
    143         if (mConference == null) {
    144             mConference = new HfpClientConference(
    145                 mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile);
    146         }
    147 
    148         if (connection1.getConference() == null) {
    149             mConference.addConnection(connection1);
    150         }
    151 
    152         if (connection2.getConference() == null) {
    153             mConference.addConnection(connection2);
    154         }
    155     }
    156 
    157     // Remove existing calls and the phone account associated, the object will get garbage
    158     // collected soon
    159     synchronized void cleanup() {
    160         Log.d(TAG, "Resetting state for device " + mDevice);
    161         disconnectAll();
    162         mTelecomManager.unregisterPhoneAccount(mPhoneAccount.getAccountHandle());
    163     }
    164 
    165     // Handle call change
    166     synchronized void handleCall(BluetoothHeadsetClientCall call) {
    167         if (DBG) {
    168             Log.d(TAG, "Got call " + call.toString(true));
    169         }
    170 
    171         HfpClientConnection connection = findConnectionKey(call);
    172 
    173         // We need to have special handling for calls that mysteriously convert from
    174         // DISCONNECTING -> ACTIVE/INCOMING state. This can happen for PTS (b/31159015).
    175         // We terminate the previous call and create a new one here.
    176         if (connection != null && isDisconnectingToActive(connection, call)) {
    177             connection.close(DisconnectCause.ERROR);
    178             mConnections.remove(call.getUUID());
    179             connection = null;
    180         }
    181 
    182         if (connection != null) {
    183             connection.updateCall(call);
    184             connection.handleCallChanged();
    185         }
    186 
    187         if (connection == null) {
    188             // Create the connection here, trigger Telecom to bind to us.
    189             buildConnection(call, null);
    190 
    191             // Depending on where this call originated make it an incoming call or outgoing
    192             // (represented as unknown call in telecom since). Since BluetoothHeadsetClientCall is a
    193             // parcelable we simply pack the entire object in there.
    194             Bundle b = new Bundle();
    195             if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_DIALING ||
    196                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ALERTING ||
    197                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_ACTIVE ||
    198                 call.getState() == BluetoothHeadsetClientCall.CALL_STATE_WAITING) {
    199                 // This is an outgoing call. Even if it is an active call we do not have a way of
    200                 // putting that parcelable in a seaprate field.
    201                 b.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, call);
    202                 mTelecomManager.addNewUnknownCall(mPhoneAccount.getAccountHandle(), b);
    203             } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_INCOMING) {
    204                 // This is an incoming call.
    205                 b.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, call);
    206                 mTelecomManager.addNewIncomingCall(mPhoneAccount.getAccountHandle(), b);
    207             }
    208         } else if (call.getState() == BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
    209             if (DBG) {
    210                 Log.d(TAG, "Removing call " + call);
    211             }
    212             mConnections.remove(call.getUUID());
    213         }
    214 
    215         updateConferenceableConnections();
    216     }
    217 
    218     // Find the connection specified by the key, also update the key with ID if present.
    219     private synchronized HfpClientConnection findConnectionKey(BluetoothHeadsetClientCall call) {
    220         if (DBG) {
    221             Log.d(TAG, "findConnectionKey local key set " + mConnections.toString());
    222         }
    223         return mConnections.get(call.getUUID());
    224     }
    225 
    226     // Disconnect all calls
    227     private void disconnectAll() {
    228         for (HfpClientConnection connection : mConnections.values()) {
    229             connection.onHfpDisconnected();
    230         }
    231 
    232         mConnections.clear();
    233 
    234         if (mConference != null) {
    235             mConference.destroy();
    236             mConference = null;
    237         }
    238     }
    239 
    240     private boolean isDisconnectingToActive(HfpClientConnection prevConn,
    241             BluetoothHeadsetClientCall newCall) {
    242         if (DBG) {
    243             Log.d(TAG, "prevConn " + prevConn.isClosing() + " new call " + newCall.getState());
    244         }
    245         if (prevConn.isClosing() &&
    246                 newCall.getState() != BluetoothHeadsetClientCall.CALL_STATE_TERMINATED) {
    247             return true;
    248         }
    249         return false;
    250     }
    251 
    252     private synchronized HfpClientConnection buildConnection(
    253             BluetoothHeadsetClientCall call, Uri number) {
    254         if (mHeadsetProfile == null) {
    255             Log.e(TAG, "Cannot create connection for call " + call + " when Profile not available");
    256             return null;
    257         }
    258 
    259         if (call == null && number == null) {
    260             Log.e(TAG, "Both call and number cannot be null.");
    261             return null;
    262         }
    263 
    264         if (DBG) {
    265             Log.d(TAG, "Creating connection on " + mDevice + " for " + call + "/" + number);
    266         }
    267 
    268         HfpClientConnection connection = null;
    269         if (call != null) {
    270             connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, call);
    271         } else {
    272             connection = new HfpClientConnection(mConnServ, mDevice, mHeadsetProfile, number);
    273         }
    274 
    275         if (connection.getState() != Connection.STATE_DISCONNECTED) {
    276             mConnections.put(connection.getUUID(), connection);
    277         }
    278 
    279         return connection;
    280     }
    281 
    282     // Updates any conferencable connections.
    283     private void updateConferenceableConnections() {
    284         boolean addConf = false;
    285         if (DBG) {
    286             Log.d(TAG, "Existing connections: " + mConnections + " existing conference " +
    287                 mConference);
    288         }
    289 
    290         // If we have an existing conference call then loop through all connections and update any
    291         // connections that may have switched from conference -> non-conference.
    292         if (mConference != null) {
    293             for (Connection confConn : mConference.getConnections()) {
    294                 if (!((HfpClientConnection) confConn).inConference()) {
    295                     if (DBG) {
    296                         Log.d(TAG, "Removing connection " + confConn + " from conference.");
    297                     }
    298                     mConference.removeConnection(confConn);
    299                 }
    300             }
    301         }
    302 
    303         // If we have connections that are not already part of the conference then add them.
    304         // NOTE: addConnection takes care of duplicates (by mem addr) and the lifecycle of a
    305         // connection is maintained by the UUID.
    306         for (Connection otherConn : mConnections.values()) {
    307             if (((HfpClientConnection) otherConn).inConference()) {
    308                 // If this is the first connection with conference, create the conference first.
    309                 if (mConference == null) {
    310                     mConference = new HfpClientConference(
    311                         mPhoneAccount.getAccountHandle(), mDevice, mHeadsetProfile);
    312                 }
    313                 if (mConference.addConnection(otherConn)) {
    314                     if (DBG) {
    315                         Log.d(TAG, "Adding connection " + otherConn + " to conference.");
    316                     }
    317                     addConf = true;
    318                 }
    319             }
    320         }
    321 
    322         // If we have no connections in the conference we should simply end it.
    323         if (mConference != null && mConference.getConnections().size() == 0) {
    324             if (DBG) {
    325                 Log.d(TAG, "Conference has no connection, destroying");
    326             }
    327             mConference.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
    328             mConference.destroy();
    329             mConference = null;
    330         }
    331 
    332         // If we have a valid conference and not previously added then add it.
    333         if (mConference != null && addConf) {
    334             if (DBG) {
    335                 Log.d(TAG, "Adding conference to stack.");
    336             }
    337             mConnServ.addConference(mConference);
    338         }
    339     }
    340 
    341 }
    342