Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a9ad98ec authored by Sanket Agarwal's avatar Sanket Agarwal
Browse files

HFP is exposed via Telecom ConnectionService.

Telecom provides a ConnectionService mechanism where in order to make
calls you only need to provide the right phone account (which can be
queried using a predefined scheme).

This change adds a new middleware called ConnectionService which
provides a translation medium from Bluetooth <-> Telecom. Anyone who
wishes to use HFP HF role can then simply use TelecomManager (and
TelecomManager's InCallService) interfaces.

Bug: b/26757899

Change-Id: I66e47b6ff6330cfd9040a4a6cf4edadac28d63de
parent b03b1ce3
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -342,5 +342,13 @@
                <action android:name="android.bluetooth.IBluetoothHeadsetClient" />
            </intent-filter>
        </service>
        <service android:name=".hfpclient.connserv.HfpClientConnectionService"
                 android:permission="android.permission.BIND_CONNECTION_SERVICE"
                 android:enabled="@bool/profile_supported_hfpclient">
            <intent-filter>
                <!-- Mechanism for Telecom stack to connect -->
                <action android:name="android.telecom.ConnectionService" />
            </intent-filter>
        </service>
    </application>
</manifest>
+22 −3
Original line number Diff line number Diff line
@@ -51,13 +51,15 @@ import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.telecom.TelecomManager;

import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService;
import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;

import java.util.ArrayList;
import java.util.Arrays;
@@ -155,6 +157,7 @@ final class HeadsetClientStateMachine extends StateMachine {
    private boolean mAudioWbs;
    private final BluetoothAdapter mAdapter;
    private boolean mNativeAvailable;
    private TelecomManager mTelecomManager;

    // currently connected device
    private BluetoothDevice mCurrentDevice = null;
@@ -318,6 +321,7 @@ final class HeadsetClientStateMachine extends StateMachine {
    }

    private void sendCallChangedIntent(BluetoothHeadsetClientCall c) {
        Log.d(TAG, "sendCallChangedIntent " + c);
        Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
        intent.putExtra(BluetoothHeadsetClient.EXTRA_CALL, c);
        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
@@ -1214,6 +1218,8 @@ final class HeadsetClientStateMachine extends StateMachine {
        mAudioRouteAllowed = context.getResources().getBoolean(
                R.bool.headset_client_initial_audio_route_allowed);

        mTelecomManager = (TelecomManager) context.getSystemService(context.TELECOM_SERVICE);

        mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
        mIndicatorNetworkType = HeadsetClientHalConstants.SERVICE_TYPE_HOME;
        mIndicatorNetworkSignal = 0;
@@ -2344,6 +2350,19 @@ final class HeadsetClientStateMachine extends StateMachine {
                    HeadsetClientHalConstants.CHLD_FEAT_MERGE_DETACH) {
                intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_MERGE_AND_DETACH, true);
            }

            // If we are connected to HFP AG, then register the phone account so that telecom can
            // make calls via HFP.
            mTelecomManager.registerPhoneAccount(
                HfpClientConnectionService.getAccount(mService, device));
            mTelecomManager.enablePhoneAccount(
                HfpClientConnectionService.getAccount(mService, device).getAccountHandle(), true);
            mTelecomManager.setUserSelectedOutgoingPhoneAccount(
                HfpClientConnectionService.getHandle(mService));
            mService.startService(new Intent(mService, HfpClientConnectionService.class));
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            mTelecomManager.unregisterPhoneAccount(HfpClientConnectionService.getHandle(mService));
            mService.stopService(new Intent(mService, HfpClientConnectionService.class));
        }

        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
+105 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.bluetooth.hfpclient.connserv;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.os.Bundle;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import android.util.Log;

import java.util.List;
import java.util.ArrayList;

public class HfpClientConference extends Conference {
    private static final String TAG = "HfpClientConference";

    private BluetoothDevice mDevice;
    private BluetoothHeadsetClient mHeadsetProfile;

    public HfpClientConference(PhoneAccountHandle handle,
            BluetoothDevice device, BluetoothHeadsetClient client) {
        super(handle);
        mDevice = device;
        mHeadsetProfile = client;
        boolean manage = HfpClientConnectionService.hasHfpClientEcc(client, device);
        setConnectionCapabilities(Connection.CAPABILITY_SUPPORT_HOLD |
                Connection.CAPABILITY_HOLD |
                (manage ? Connection.CAPABILITY_MANAGE_CONFERENCE : 0));
        setActive();
    }

    @Override
    public void onDisconnect() {
        Log.d(TAG, "onDisconnect");
        mHeadsetProfile.terminateCall(mDevice, 0);
        setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
    }

    @Override
    public void onMerge(Connection connection) {
        Log.d(TAG, "onMerge " + connection);
        addConnection(connection);
    }

    @Override
    public void onSeparate(Connection connection) {
        Log.d(TAG, "onSeparate " + connection);
        ((HfpClientConnection) connection).enterPrivateMode();
        removeConnection(connection);
    }

    @Override
    public void onHold() {
        Log.d(TAG, "onHold");
        mHeadsetProfile.holdCall(mDevice);
    }

    @Override
    public void onUnhold() {
        if (getPrimaryConnection().getConnectionService()
                .getAllConnections().size() > 1) {
            Log.w(TAG, "Ignoring unhold; call hold on the foreground call");
            return;
        }
        Log.d(TAG, "onUnhold");
        mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_HOLD);
    }

    @Override
    public void onPlayDtmfTone(char c) {
        Log.d(TAG, "onPlayDtmfTone " + c);
        if (mHeadsetProfile != null) {
            mHeadsetProfile.sendDTMF(mDevice, (byte) c);
        }
    }

    @Override
    public void onConnectionAdded(Connection connection) {
        Log.d(TAG, "onConnectionAdded " + connection);
        if (connection.getState() == Connection.STATE_HOLDING &&
                getState() == Connection.STATE_ACTIVE) {
            connection.onAnswer();
        } else if (connection.getState() == Connection.STATE_ACTIVE &&
                getState() == Connection.STATE_HOLDING) {
            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
        }
    }
}
+235 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.bluetooth.hfpclient.connserv;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClientCall;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.net.Uri;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.TelecomManager;
import android.util.Log;

public class HfpClientConnection extends Connection {
    private static final String TAG = "HfpClientConnection";

    private final Context mContext;
    private final BluetoothDevice mDevice;

    private BluetoothHeadsetClient mHeadsetProfile;
    private BluetoothHeadsetClientCall mCurrentCall;
    private boolean mClosed;
    private boolean mLocalDisconnect;
    private boolean mClientHasEcc;
    private boolean mAdded;

    public HfpClientConnection(Context context, BluetoothDevice device, BluetoothHeadsetClient client,
            BluetoothHeadsetClientCall call, Uri number) {
        mDevice = device;
        mContext = context;
        mHeadsetProfile = client;
        mCurrentCall = call;
        if (mHeadsetProfile != null) {
            mClientHasEcc = HfpClientConnectionService.hasHfpClientEcc(mHeadsetProfile, mDevice);
        }
        setAudioModeIsVoip(false);
        setAddress(number, TelecomManager.PRESENTATION_ALLOWED);
        setInitializing();

        if (mHeadsetProfile != null) {
            finishInitializing();
        }
    }

    public void onHfpConnected(BluetoothHeadsetClient client) {
        mHeadsetProfile = client;
        mClientHasEcc = HfpClientConnectionService.hasHfpClientEcc(mHeadsetProfile, mDevice);
        finishInitializing();
    }

    public void onHfpDisconnected() {
        mHeadsetProfile = null;
        close(DisconnectCause.ERROR);
    }

    public void onAdded() {
        mAdded = true;
    }

    public BluetoothHeadsetClientCall getCall() {
        return mCurrentCall;
    }

    public boolean inConference() {
        return mAdded && mCurrentCall != null && mCurrentCall.isMultiParty() &&
                getState() != Connection.STATE_DISCONNECTED;
    }

    public void enterPrivateMode() {
        mHeadsetProfile.enterPrivateMode(mDevice, mCurrentCall.getId());
        setActive();
    }

    public void handleCallChanged(BluetoothHeadsetClientCall call) {
        HfpClientConference conference = (HfpClientConference) getConference();
        mCurrentCall = call;

        int state = call.getState();
        Log.d(TAG, "Got call state change to " + state);
        switch (state) {
            case BluetoothHeadsetClientCall.CALL_STATE_ACTIVE:
                setActive();
                if (conference != null) {
                    conference.setActive();
                }
                break;
            case BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
            case BluetoothHeadsetClientCall.CALL_STATE_HELD:
                setOnHold();
                if (conference != null) {
                    conference.setOnHold();
                }
                break;
            case BluetoothHeadsetClientCall.CALL_STATE_DIALING:
            case BluetoothHeadsetClientCall.CALL_STATE_ALERTING:
                setDialing();
                break;
            case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
            case BluetoothHeadsetClientCall.CALL_STATE_WAITING:
                setRinging();
                break;
            case BluetoothHeadsetClientCall.CALL_STATE_TERMINATED:
                // TODO Use more specific causes
                close(mLocalDisconnect ? DisconnectCause.LOCAL : DisconnectCause.REMOTE);
                break;
            default:
                Log.wtf(TAG, "Unexpected phone state " + state);
        }
        setConnectionCapabilities(CAPABILITY_SUPPORT_HOLD | CAPABILITY_MUTE |
                CAPABILITY_SEPARATE_FROM_CONFERENCE | CAPABILITY_DISCONNECT_FROM_CONFERENCE |
                (getState() == STATE_ACTIVE || getState() == STATE_HOLDING ? CAPABILITY_HOLD : 0));
    }

    private void finishInitializing() {
        if (mCurrentCall == null) {
            String number = getAddress().getSchemeSpecificPart();
            Log.d(TAG, "Dialing " + number);
            setInitialized();
            mHeadsetProfile.dial(mDevice, number);
        } else {
            handleCallChanged(mCurrentCall);
        }
    }

    private void close(int cause) {
        Log.d(TAG, "Closing " + mClosed);
        if (mClosed) {
            return;
        }
        setDisconnected(new DisconnectCause(cause));

        mClosed = true;
        mCurrentCall = null;

        destroy();
    }

    @Override
    public void onPlayDtmfTone(char c) {
        Log.d(TAG, "onPlayDtmfTone " + c + " " + mCurrentCall);
        if (!mClosed && mHeadsetProfile != null) {
            mHeadsetProfile.sendDTMF(mDevice, (byte) c);
        }
    }

    @Override
    public void onDisconnect() {
        Log.d(TAG, "onDisconnect " + mCurrentCall);
        if (!mClosed) {
            if (mHeadsetProfile != null && mCurrentCall != null) {
                mHeadsetProfile.terminateCall(mDevice, mClientHasEcc ? mCurrentCall.getId() : 0);
                mLocalDisconnect = true;
            } else {
                close(DisconnectCause.LOCAL);
            }
        }
    }

    @Override
    public void onAbort() {
        Log.d(TAG, "onAbort " + mCurrentCall);
        onDisconnect();
    }

    @Override
    public void onHold() {
        Log.d(TAG, "onHold " + mCurrentCall);
        if (!mClosed && mHeadsetProfile != null) {
            mHeadsetProfile.holdCall(mDevice);
        }
    }

    @Override
    public void onUnhold() {
        if (getConnectionService().getAllConnections().size() > 1) {
            Log.w(TAG, "Ignoring unhold; call hold on the foreground call");
            return;
        }
        Log.d(TAG, "onUnhold " + mCurrentCall);
        if (!mClosed && mHeadsetProfile != null) {
            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_HOLD);
        }
    }

    @Override
    public void onAnswer() {
        Log.d(TAG, "onAnswer " + mCurrentCall);
        if (!mClosed) {
            mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
        }
    }

    @Override
    public void onReject() {
        Log.d(TAG, "onReject " + mCurrentCall);
        if (!mClosed) {
            mHeadsetProfile.rejectCall(mDevice);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof HfpClientConnection)) {
            return false;
        }
        Uri otherAddr = ((HfpClientConnection) o).getAddress();
        return getAddress() == otherAddr || otherAddr != null && otherAddr.equals(getAddress());
    }

    @Override
    public int hashCode() {
        return getAddress() == null ? 0 : getAddress().hashCode();
    }

    @Override
    public String toString() {
        return "HfpClientConnection{" + getAddress() + "," + stateToString(getState()) + "," +
                mCurrentCall + "}";
    }
}
+412 −0

File added.

Preview size limit exceeded, changes collapsed.