/* * 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.annotation.RequiresPermission; import android.annotation.SuppressLint; 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 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)); } @SuppressLint("AndroidFrameworkRequiresPermission") 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(); } } } @SuppressLint("AndroidFrameworkRequiresPermission") 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(); } } } }