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