blob: 941ec1418feca1e4b4c1be8cd0e97dd947d6a31b [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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Looper;
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 com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.TelephonyIntents;
import java.util.HashMap;
import java.util.Objects;
/**
* 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 ServiceState mServiceState;
// HFP 1.6 CIND service value
private int mCindService = HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE;
// Check this before sending out service state to the device -- if the SIM isn't fully
// loaded, don't expose that the network is available.
private boolean mIsSimStateLoaded;
// 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
mOnSubscriptionsChangedListener = new HeadsetPhoneStateOnSubscriptionChangedListener(
headsetService.getStateMachinesThreadLooper());
mSubscriptionManager.addOnSubscriptionsChangedListener(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(
mHeadsetService.getStateMachinesThreadLooper());
mTelephonyManager.listen(mPhoneStateListener, events);
if ((events & PhoneStateListener.LISTEN_SIGNAL_STRENGTHS) != 0) {
mTelephonyManager.setRadioIndicationUpdateMode(
TelephonyManager.INDICATION_FILTER_SIGNAL_STRENGTH,
TelephonyManager.INDICATION_UPDATE_MODE_IGNORE_SCREEN_OFF);
}
}
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);
mTelephonyManager.setRadioIndicationUpdateMode(
TelephonyManager.INDICATION_FILTER_SIGNAL_STRENGTH,
TelephonyManager.INDICATION_UPDATE_MODE_NORMAL);
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;
}
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 void sendDeviceStateChanged() {
int service =
mIsSimStateLoaded ? mCindService : HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE;
// When out of service, send signal strength as 0. Some devices don't
// use the service indicator, but only the signal indicator
int signal = service == HeadsetHalConstants.NETWORK_STATE_AVAILABLE ? mCindSignal : 0;
Log.d(TAG, "sendDeviceStateChanged. mService=" + mCindService + " mIsSimStateLoaded="
+ mIsSimStateLoaded + " mSignal=" + signal + " mRoam=" + mCindRoam
+ " mBatteryCharge=" + mCindBatteryCharge);
mHeadsetService.onDeviceStateChanged(
new HeadsetDeviceState(service, mCindRoam, signal, mCindBatteryCharge));
}
private class HeadsetPhoneStateOnSubscriptionChangedListener
extends OnSubscriptionsChangedListener {
HeadsetPhoneStateOnSubscriptionChangedListener(Looper looper) {
super(looper);
}
@Override
public void onSubscriptionsChanged() {
synchronized (mDeviceEventMap) {
stopListenForPhoneState();
startListenForPhoneState();
}
}
}
private class HeadsetPhoneStateListener extends PhoneStateListener {
HeadsetPhoneStateListener(Looper looper) {
super(looper);
}
@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;
// If this is due to a SIM insertion, we want to defer sending device state changed
// until all the SIM config is loaded.
if (cindService == HeadsetHalConstants.NETWORK_STATE_NOT_AVAILABLE) {
mIsSimStateLoaded = false;
sendDeviceStateChanged();
return;
}
IntentFilter simStateChangedFilter =
new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
mHeadsetService.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(intent.getAction())) {
// This is a sticky broadcast, so if it's already been loaded,
// this'll execute immediately.
if (IccCardConstants.INTENT_VALUE_ICC_LOADED.equals(
intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE))) {
mIsSimStateLoaded = true;
sendDeviceStateChanged();
mHeadsetService.unregisterReceiver(this);
}
}
}
}, simStateChangedFilter);
}
@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();
}
}
}
}