blob: d1dbe3242d0e728ee43cb5a477ffef58c02e8317 [file] [log] [blame]
/*
* Copyright (C) 2012 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.hfp;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* Class that manages Telephony states
*
* Note:
* The methods in this class are not thread safe, don't call them from
* multiple threads. Call them from the HeadsetPhoneStateMachine message
* handler only.
*/
public class HeadsetPhoneState {
private static final String TAG = "HeadsetPhoneState";
private final HeadsetService mHeadsetService;
private final TelephonyManager mTelephonyManager;
private final SubscriptionManager mSubscriptionManager;
private final Handler mHandler;
private ServiceState mServiceState;
// HFP 1.6 CIND service value
private int mCindService = HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE;
// Number of active (foreground) calls
private int mNumActive;
// Current Call Setup State
private int mCallState = HeadsetHalConstants.CALL_STATE_IDLE;
// Number of held (background) calls
private int mNumHeld;
// HFP 1.6 CIND signal value
private int mCindSignal;
// HFP 1.6 CIND roam value
private int mCindRoam = HeadsetHalConstants.SERVICE_TYPE_HOME;
// HFP 1.6 CIND battchg value
private int mCindBatteryCharge;
private final HashMap<BluetoothDevice, Integer> mDeviceEventMap = new HashMap<>();
private PhoneStateListener mPhoneStateListener;
private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
HeadsetPhoneState(HeadsetService headsetService) {
Objects.requireNonNull(headsetService, "headsetService is null");
mHeadsetService = headsetService;
mTelephonyManager =
(TelephonyManager) mHeadsetService.getSystemService(Context.TELEPHONY_SERVICE);
Objects.requireNonNull(mTelephonyManager, "TELEPHONY_SERVICE is null");
// Register for SubscriptionInfo list changes which is guaranteed to invoke
// onSubscriptionInfoChanged and which in turns calls loadInBackgroud.
mSubscriptionManager = SubscriptionManager.from(mHeadsetService);
Objects.requireNonNull(mSubscriptionManager, "TELEPHONY_SUBSCRIPTION_SERVICE is null");
// Initialize subscription on the handler thread
mHandler = new Handler(headsetService.getStateMachinesThreadLooper());
mOnSubscriptionsChangedListener = new HeadsetPhoneStateOnSubscriptionChangedListener();
mSubscriptionManager.addOnSubscriptionsChangedListener(command -> mHandler.post(command),
mOnSubscriptionsChangedListener);
}
/**
* Cleanup this instance. Instance can no longer be used after calling this method.
*/
public void cleanup() {
synchronized (mDeviceEventMap) {
mDeviceEventMap.clear();
stopListenForPhoneState();
}
mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
}
@Override
public String toString() {
return "HeadsetPhoneState [mTelephonyServiceAvailability=" + mCindService + ", mNumActive="
+ mNumActive + ", mCallState=" + mCallState + ", mNumHeld=" + mNumHeld
+ ", mSignal=" + mCindSignal + ", mRoam=" + mCindRoam + ", mBatteryCharge="
+ mCindBatteryCharge + ", TelephonyEvents=" + getTelephonyEventsToListen() + "]";
}
private int getTelephonyEventsToListen() {
synchronized (mDeviceEventMap) {
return mDeviceEventMap.values()
.stream()
.reduce(PhoneStateListener.LISTEN_NONE, (a, b) -> a | b);
}
}
/**
* Start or stop listening for phone state change
*
* @param device remote device that subscribes to this phone state update
* @param events events in {@link PhoneStateListener} to listen to
*/
@VisibleForTesting
public void listenForPhoneState(BluetoothDevice device, int events) {
synchronized (mDeviceEventMap) {
int prevEvents = getTelephonyEventsToListen();
if (events == PhoneStateListener.LISTEN_NONE) {
mDeviceEventMap.remove(device);
} else {
mDeviceEventMap.put(device, events);
}
int updatedEvents = getTelephonyEventsToListen();
if (prevEvents != updatedEvents) {
stopListenForPhoneState();
startListenForPhoneState();
}
}
}
private void startListenForPhoneState() {
if (mPhoneStateListener != null) {
Log.w(TAG, "startListenForPhoneState, already listening");
return;
}
int events = getTelephonyEventsToListen();
if (events == PhoneStateListener.LISTEN_NONE) {
Log.w(TAG, "startListenForPhoneState, no event to listen");
return;
}
int subId = SubscriptionManager.getDefaultSubscriptionId();
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
// Will retry listening for phone state in onSubscriptionsChanged() callback
Log.w(TAG, "startListenForPhoneState, invalid subscription ID " + subId);
return;
}
Log.i(TAG, "startListenForPhoneState(), subId=" + subId + ", enabled_events=" + events);
mPhoneStateListener = new HeadsetPhoneStateListener(command -> mHandler.post(command));
mTelephonyManager.listen(mPhoneStateListener, events);
}
private void stopListenForPhoneState() {
if (mPhoneStateListener == null) {
Log.i(TAG, "stopListenForPhoneState(), no listener indicates nothing is listening");
return;
}
Log.i(TAG, "stopListenForPhoneState(), stopping listener, enabled_events="
+ getTelephonyEventsToListen());
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
mPhoneStateListener = null;
}
int getCindService() {
return mCindService;
}
int getNumActiveCall() {
return mNumActive;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setNumActiveCall(int numActive) {
mNumActive = numActive;
}
int getCallState() {
return mCallState;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setCallState(int callState) {
mCallState = callState;
}
int getNumHeldCall() {
return mNumHeld;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setNumHeldCall(int numHeldCall) {
mNumHeld = numHeldCall;
}
ServiceState getServiceState() {
return mServiceState;
}
int getCindSignal() {
return mCindSignal;
}
int getCindRoam() {
return mCindRoam;
}
/**
* Set battery level value used for +CIND result
*
* @param batteryLevel battery level value
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setCindBatteryCharge(int batteryLevel) {
if (mCindBatteryCharge != batteryLevel) {
mCindBatteryCharge = batteryLevel;
sendDeviceStateChanged();
}
}
int getCindBatteryCharge() {
return mCindBatteryCharge;
}
boolean isInCall() {
return (mNumActive >= 1);
}
private synchronized void sendDeviceStateChanged() {
// When out of service, send signal strength as 0. Some devices don't
// use the service indicator, but only the signal indicator
int signal = mCindService == HeadsetHalConstants.NETWORK_STATE_AVAILABLE ? mCindSignal : 0;
Log.d(TAG, "sendDeviceStateChanged. mService=" + mCindService
+ " mSignal=" + mCindSignal + " mRoam=" + mCindRoam
+ " mBatteryCharge=" + mCindBatteryCharge);
mHeadsetService.onDeviceStateChanged(
new HeadsetDeviceState(mCindService, mCindRoam, signal, mCindBatteryCharge));
}
private class HeadsetPhoneStateOnSubscriptionChangedListener
extends OnSubscriptionsChangedListener {
HeadsetPhoneStateOnSubscriptionChangedListener() {
super();
}
@Override
public void onSubscriptionsChanged() {
synchronized (mDeviceEventMap) {
int simState = mTelephonyManager.getSimState();
if (simState != TelephonyManager.SIM_STATE_READY) {
mServiceState = null;
mCindSignal = 0;
mCindService = HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE;
sendDeviceStateChanged();
}
stopListenForPhoneState();
startListenForPhoneState();
}
}
}
private class HeadsetPhoneStateListener extends PhoneStateListener {
HeadsetPhoneStateListener(Executor executor) {
super(executor);
}
@Override
public synchronized void onServiceStateChanged(ServiceState serviceState) {
mServiceState = serviceState;
int cindService = (serviceState.getState() == ServiceState.STATE_IN_SERVICE)
? HeadsetHalConstants.NETWORK_STATE_AVAILABLE
: HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE;
int newRoam = serviceState.getRoaming() ? HeadsetHalConstants.SERVICE_TYPE_ROAMING
: HeadsetHalConstants.SERVICE_TYPE_HOME;
if (cindService == mCindService && newRoam == mCindRoam) {
// De-bounce the state change
return;
}
mCindService = cindService;
mCindRoam = newRoam;
sendDeviceStateChanged();
}
@Override
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
int prevSignal = mCindSignal;
if (mCindService == HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE) {
mCindSignal = 0;
} else {
mCindSignal = signalStrength.getLevel() + 1;
}
// +CIND "signal" indicator is always between 0 to 5
mCindSignal = Integer.max(Integer.min(mCindSignal, 5), 0);
// This results in a lot of duplicate messages, hence this check
if (prevSignal != mCindSignal) {
sendDeviceStateChanged();
}
}
}
}