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.BluetoothDevice;
19import android.bluetooth.BluetoothHeadsetClient;
20import android.bluetooth.BluetoothHeadsetClientCall;
21import android.bluetooth.BluetoothProfile;
22import android.content.Context;
23import android.net.Uri;
24import android.telecom.Connection;
25import android.telecom.DisconnectCause;
26import android.telecom.PhoneAccount;
27import android.telecom.TelecomManager;
28import android.util.Log;
29
30import java.util.UUID;
31
32public class HfpClientConnection extends Connection {
33    private static final String TAG = "HfpClientConnection";
34    private static final boolean DBG = false;
35
36    private final Context mContext;
37    private final BluetoothDevice mDevice;
38    private BluetoothHeadsetClient mHeadsetProfile;
39
40    private BluetoothHeadsetClientCall mCurrentCall;
41    private int mPreviousCallState = -1;
42    private boolean mClosed;
43    private boolean mClosing = false;
44    private boolean mLocalDisconnect;
45    private boolean mClientHasEcc;
46    private boolean mAdded;
47
48    // Constructor to be used when there's an existing call (such as that created on the AG or
49    // when connection happens and we see calls for the first time).
50    public HfpClientConnection(Context context, BluetoothDevice device,
51            BluetoothHeadsetClient client, BluetoothHeadsetClientCall call) {
52        mDevice = device;
53        mContext = context;
54        mHeadsetProfile = client;
55
56        if (call == null) {
57            throw new IllegalStateException("Call is null");
58        }
59
60        mCurrentCall = call;
61        handleCallChanged();
62        finishInitializing();
63    }
64
65    // Constructor to be used when a call is intiated on the HF. The call handle is obtained by
66    // using the dial() command.
67    public HfpClientConnection(Context context, BluetoothDevice device,
68            BluetoothHeadsetClient client, Uri number) {
69        mDevice = device;
70        mContext = context;
71        mHeadsetProfile = client;
72
73        if (mHeadsetProfile == null) {
74            throw new IllegalStateException("HeadsetProfile is null, returning");
75        }
76
77        mCurrentCall = mHeadsetProfile.dial(
78            mDevice, number.getSchemeSpecificPart());
79        if (mCurrentCall == null) {
80            close(DisconnectCause.ERROR);
81            Log.e(TAG, "Failed to create the call, dial failed.");
82            return;
83        }
84
85        mHeadsetProfile.connectAudio(device);
86        setInitializing();
87        setDialing();
88        finishInitializing();
89    }
90
91    void finishInitializing() {
92        mClientHasEcc = HfpClientConnectionService.hasHfpClientEcc(mHeadsetProfile, mDevice);
93        setAudioModeIsVoip(false);
94        Uri number = Uri.fromParts(PhoneAccount.SCHEME_TEL, mCurrentCall.getNumber(), null);
95        setAddress(number, TelecomManager.PRESENTATION_ALLOWED);
96        setConnectionCapabilities(CAPABILITY_SUPPORT_HOLD | CAPABILITY_MUTE |
97                CAPABILITY_SEPARATE_FROM_CONFERENCE | CAPABILITY_DISCONNECT_FROM_CONFERENCE |
98                (getState() == STATE_ACTIVE || getState() == STATE_HOLDING ? CAPABILITY_HOLD : 0));
99    }
100
101    public UUID getUUID() {
102        return mCurrentCall.getUUID();
103    }
104
105    public void onHfpDisconnected() {
106        mHeadsetProfile = null;
107        close(DisconnectCause.ERROR);
108    }
109
110    public void onAdded() {
111        mAdded = true;
112    }
113
114    public BluetoothHeadsetClientCall getCall() {
115        return mCurrentCall;
116    }
117
118    public boolean inConference() {
119        return mAdded && mCurrentCall != null && mCurrentCall.isMultiParty() &&
120                getState() != Connection.STATE_DISCONNECTED;
121    }
122
123    public void enterPrivateMode() {
124        mHeadsetProfile.enterPrivateMode(mDevice, mCurrentCall.getId());
125        setActive();
126    }
127
128    public void updateCall(BluetoothHeadsetClientCall call) {
129        if (call == null) {
130            Log.e(TAG, "Updating call to a null value.");
131            return;
132        }
133        mCurrentCall = call;
134    }
135
136    public void handleCallChanged() {
137        HfpClientConference conference = (HfpClientConference) getConference();
138        int state = mCurrentCall.getState();
139
140        if (DBG) {
141            Log.d(TAG, "Got call state change to " + state);
142        }
143        switch (state) {
144            case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE:
145                setActive();
146                if (conference != null) {
147                    conference.setActive();
148                }
149                break;
150            case BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
151            case BluetoothHeadsetClientCall.CALL_STATE_HELD:
152                setOnHold();
153                if (conference != null) {
154                    conference.setOnHold();
155                }
156                break;
157            case BluetoothHeadsetClientCall.CALL_STATE_DIALING:
158            case BluetoothHeadsetClientCall.CALL_STATE_ALERTING:
159                setDialing();
160                break;
161            case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
162            case BluetoothHeadsetClientCall.CALL_STATE_WAITING:
163                setRinging();
164                break;
165            case BluetoothHeadsetClientCall.CALL_STATE_TERMINATED:
166                if (mPreviousCallState == BluetoothHeadsetClientCall.CALL_STATE_INCOMING
167                        || mPreviousCallState == BluetoothHeadsetClientCall.CALL_STATE_WAITING) {
168                    close(DisconnectCause.MISSED);
169                } else if (mLocalDisconnect) {
170                    close(DisconnectCause.LOCAL);
171                } else {
172                    close(DisconnectCause.REMOTE);
173                }
174                break;
175            default:
176                Log.wtf(TAG, "Unexpected phone state " + state);
177        }
178        mPreviousCallState = state;
179    }
180
181    public synchronized void close(int cause) {
182        if (DBG) {
183            Log.d(TAG, "Closing call " + mCurrentCall + "state: " + mClosed);
184        }
185        if (mClosed) {
186            return;
187        }
188        Log.d(TAG, "Setting " + mCurrentCall + " to disconnected " + getTelecomCallId());
189        setDisconnected(new DisconnectCause(cause));
190
191        mClosed = true;
192        mCurrentCall = null;
193
194        destroy();
195    }
196
197    public synchronized boolean isClosing() {
198        return mClosing;
199    }
200
201    public synchronized BluetoothDevice getDevice() {
202        return mDevice;
203    }
204
205    @Override
206    public synchronized void onPlayDtmfTone(char c) {
207        if (DBG) {
208            Log.d(TAG, "onPlayDtmfTone " + c + " " + mCurrentCall);
209        }
210        if (!mClosed) {
211            mHeadsetProfile.sendDTMF(mDevice, (byte) c);
212        }
213    }
214
215    @Override
216    public synchronized void onDisconnect() {
217        if (DBG) {
218            Log.d(TAG, "onDisconnect call: " + mCurrentCall + " state: " + mClosed);
219        }
220        // The call is not closed so we should send a terminate here.
221        if (!mClosed) {
222            mHeadsetProfile.terminateCall(mDevice, mCurrentCall);
223            mLocalDisconnect = true;
224            mClosing = true;
225        }
226    }
227
228    @Override
229    public void onAbort() {
230        if (DBG) {
231            Log.d(TAG, "onAbort " + mCurrentCall);
232        }
233        onDisconnect();
234    }
235
236    @Override
237    public synchronized void onHold() {
238        if (DBG) {
239            Log.d(TAG, "onHold " + mCurrentCall);
240        }
241        if (!mClosed) {
242            mHeadsetProfile.holdCall(mDevice);
243        }
244    }
245
246    @Override
247    public synchronized void onUnhold() {
248        if (getConnectionService().getAllConnections().size() > 1) {
249            Log.w(TAG, "Ignoring unhold; call hold on the foreground call");
250            return;
251        }
252        if (DBG) {
253            Log.d(TAG, "onUnhold " + mCurrentCall);
254        }
255        if (!mClosed) {
256            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_HOLD);
257        }
258    }
259
260    @Override
261    public synchronized void onAnswer() {
262        if (DBG) {
263            Log.d(TAG, "onAnswer " + mCurrentCall);
264        }
265        if (!mClosed) {
266            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
267        }
268        mHeadsetProfile.connectAudio(mDevice);
269    }
270
271    @Override
272    public synchronized void onReject() {
273        if (DBG) {
274            Log.d(TAG, "onReject " + mCurrentCall);
275        }
276        if (!mClosed) {
277            mHeadsetProfile.rejectCall(mDevice);
278        }
279    }
280
281    @Override
282    public boolean equals(Object o) {
283        if (!(o instanceof HfpClientConnection)) {
284            return false;
285        }
286        Uri otherAddr = ((HfpClientConnection) o).getAddress();
287        return getAddress() == otherAddr || otherAddr != null && otherAddr.equals(getAddress());
288    }
289
290    @Override
291    public String toString() {
292        return "HfpClientConnection{" + getAddress() + "," + stateToString(getState()) + "," +
293                mCurrentCall + "}";
294    }
295}
296