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