/* * Copyright (C) 2018 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.server.wifi; import static android.telephony.TelephonyManager.CALL_STATE_IDLE; import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK; import static android.telephony.TelephonyManager.CALL_STATE_RINGING; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.Looper; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import com.android.modules.utils.HandlerExecutor; import com.android.wifi.resources.R; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.List; /** * This class provides the Support for SAR to control WiFi TX power limits. * It deals with the following: * - Tracking the STA state through calls from the ClientModeManager. * - Tracking the SAP state through calls from SoftApManager * - Tracking the Scan-Only state through ScanOnlyModeManager * - Tracking the state of the Cellular calls or data. * - It constructs the sar info and send it towards the HAL */ public class SarManager { // Period for checking on voice steam active (in ms) private static final int CHECK_VOICE_STREAM_INTERVAL_MS = 5000; /** * @hide constants copied over from {@link AudioManager} * TODO(b/144250387): Migrate to public API */ private static final String STREAM_DEVICES_CHANGED_ACTION = "android.media.STREAM_DEVICES_CHANGED_ACTION"; private static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE"; private static final String EXTRA_VOLUME_STREAM_DEVICES = "android.media.EXTRA_VOLUME_STREAM_DEVICES"; private static final String EXTRA_PREV_VOLUME_STREAM_DEVICES = "android.media.EXTRA_PREV_VOLUME_STREAM_DEVICES"; private static final int DEVICE_OUT_EARPIECE = 0x1; /* For Logging */ private static final String TAG = "WifiSarManager"; private boolean mVerboseLoggingEnabled = true; private SarInfo mSarInfo; /* Configuration for SAR support */ private boolean mSupportSarTxPowerLimit; private boolean mSupportSarVoiceCall; private boolean mSupportSarSoftAp; // Device starts with screen on private boolean mScreenOn = false; private boolean mIsVoiceStreamCheckEnabled = false; /** * Other parameters passed in or created in the constructor. */ private final Context mContext; private final TelephonyManager mTelephonyManager; private final AudioManager mAudioManager; private final WifiPhoneStateListener mPhoneStateListener; private final WifiNative mWifiNative; private final Handler mHandler; /** Create new instance of SarManager. */ SarManager( Context context, TelephonyManager telephonyManager, Looper looper, WifiNative wifiNative, WifiDeviceStateChangeManager wifiDeviceStateChangeManager) { mContext = context; mTelephonyManager = telephonyManager; mWifiNative = wifiNative; mAudioManager = mContext.getSystemService(AudioManager.class); mHandler = new Handler(looper); mPhoneStateListener = new WifiPhoneStateListener(looper); wifiDeviceStateChangeManager.registerStateChangeCallback( new WifiDeviceStateChangeManager.StateChangeCallback() { @Override public void onScreenStateChanged(boolean screenOn) { handleScreenStateChanged(screenOn); } }); } /** * Handle boot completed, read config flags. */ public void handleBootCompleted() { readSarConfigs(); if (mSupportSarTxPowerLimit) { mSarInfo = new SarInfo(); setSarConfigsInInfo(); registerListeners(); } } /** * Notify SarManager of screen status change */ private void handleScreenStateChanged(boolean screenOn) { if (!mSupportSarVoiceCall) { return; } if (mScreenOn == screenOn) { return; } if (mVerboseLoggingEnabled) { Log.d(TAG, "handleScreenStateChanged: screenOn = " + screenOn); } mScreenOn = screenOn; // Only schedule a voice stream check if screen is turning on, and it is currently not // scheduled if (mScreenOn && !mIsVoiceStreamCheckEnabled) { mHandler.post(() -> { checkAudioDevice(); }); mIsVoiceStreamCheckEnabled = true; } } private boolean isVoiceCallOnEarpiece() { final AudioAttributes voiceCallAttr = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .build(); List devices = mAudioManager.getDevicesForAttributes(voiceCallAttr); for (AudioDeviceAttributes device : devices) { if (device.getRole() == AudioDeviceAttributes.ROLE_OUTPUT && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { return true; } } return false; } private boolean isVoiceCallStreamActive() { int mode = mAudioManager.getMode(); return mode == AudioManager.MODE_IN_COMMUNICATION || mode == AudioManager.MODE_IN_CALL; } private void checkAudioDevice() { // First Check if audio stream is on boolean voiceStreamActive = isVoiceCallStreamActive(); boolean earPieceActive; if (voiceStreamActive) { // Check on the audio route earPieceActive = isVoiceCallOnEarpiece(); if (mVerboseLoggingEnabled) { Log.d(TAG, "EarPiece active = " + earPieceActive); } } else { earPieceActive = false; } // If audio route has changed, update SAR if (earPieceActive != mSarInfo.isEarPieceActive) { mSarInfo.isEarPieceActive = earPieceActive; updateSarScenario(); } // Now should we proceed with the checks if (!mScreenOn && !voiceStreamActive) { // No need to continue checking mIsVoiceStreamCheckEnabled = false; } else { // Schedule another check mHandler.postDelayed(() -> { checkAudioDevice(); }, CHECK_VOICE_STREAM_INTERVAL_MS); } } private void readSarConfigs() { mSupportSarTxPowerLimit = mContext.getResources().getBoolean( R.bool.config_wifi_framework_enable_sar_tx_power_limit); /* In case SAR is disabled, then all SAR inputs are automatically disabled as well (irrespective of the config) */ if (!mSupportSarTxPowerLimit) { mSupportSarVoiceCall = false; mSupportSarSoftAp = false; return; } /* Voice calls are supported when SAR is supported */ mSupportSarVoiceCall = true; mSupportSarSoftAp = mContext.getResources().getBoolean( R.bool.config_wifi_framework_enable_soft_ap_sar_tx_power_limit); } private void setSarConfigsInInfo() { mSarInfo.sarVoiceCallSupported = mSupportSarVoiceCall; mSarInfo.sarSapSupported = mSupportSarSoftAp; } private void registerListeners() { if (mSupportSarVoiceCall) { /* Listen for Phone State changes */ registerPhoneStateListener(); registerVoiceStreamListener(); } } private void registerVoiceStreamListener() { Log.i(TAG, "Registering for voice stream status"); // Register for listening to transitions of change of voice stream devices IntentFilter filter = new IntentFilter(); filter.addAction(STREAM_DEVICES_CHANGED_ACTION); mContext.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { boolean voiceStreamActive = isVoiceCallStreamActive(); if (!voiceStreamActive) { // No need to proceed, there is no voice call ongoing return; } String action = intent.getAction(); int streamType = intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1); int device = intent.getIntExtra(EXTRA_VOLUME_STREAM_DEVICES, -1); int oldDevice = intent.getIntExtra(EXTRA_PREV_VOLUME_STREAM_DEVICES, -1); if (streamType == AudioManager.STREAM_VOICE_CALL) { boolean earPieceActive = mSarInfo.isEarPieceActive; if (device == DEVICE_OUT_EARPIECE) { if (mVerboseLoggingEnabled) { Log.d(TAG, "Switching to earpiece : HEAD ON"); Log.d(TAG, "Old device = " + oldDevice); } earPieceActive = true; } else if (oldDevice == DEVICE_OUT_EARPIECE) { if (mVerboseLoggingEnabled) { Log.d(TAG, "Switching from earpiece : HEAD OFF"); Log.d(TAG, "New device = " + device); } earPieceActive = false; } if (earPieceActive != mSarInfo.isEarPieceActive) { mSarInfo.isEarPieceActive = earPieceActive; updateSarScenario(); } } } }, filter, null, mHandler); } /** * Register the phone state listener. */ private void registerPhoneStateListener() { Log.i(TAG, "Registering for telephony call state changes"); mTelephonyManager.listen( mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } /** * Update Wifi Client State */ public void setClientWifiState(int state) { boolean newIsEnabled; /* No action is taken if SAR is not supported */ if (!mSupportSarTxPowerLimit) { return; } if (state == WifiManager.WIFI_STATE_DISABLED) { newIsEnabled = false; } else if (state == WifiManager.WIFI_STATE_ENABLED) { newIsEnabled = true; } else { /* No change so exiting with no action */ return; } /* Report change to HAL if needed */ if (mSarInfo.isWifiClientEnabled != newIsEnabled) { mSarInfo.isWifiClientEnabled = newIsEnabled; updateSarScenario(); } } /** * Update Wifi SoftAP State */ public void setSapWifiState(int state) { boolean newIsEnabled; /* No action is taken if SAR is not supported */ if (!mSupportSarTxPowerLimit) { return; } if (state == WifiManager.WIFI_AP_STATE_DISABLED) { newIsEnabled = false; } else if (state == WifiManager.WIFI_AP_STATE_ENABLED) { newIsEnabled = true; } else { /* No change so exiting with no action */ return; } /* Report change to HAL if needed */ if (mSarInfo.isWifiSapEnabled != newIsEnabled) { mSarInfo.isWifiSapEnabled = newIsEnabled; updateSarScenario(); } } /** * Update Wifi ScanOnly State */ public void setScanOnlyWifiState(int state) { boolean newIsEnabled; /* No action is taken if SAR is not supported */ if (!mSupportSarTxPowerLimit) { return; } if (state == WifiManager.WIFI_STATE_DISABLED) { newIsEnabled = false; } else if (state == WifiManager.WIFI_STATE_ENABLED) { newIsEnabled = true; } else { /* No change so exiting with no action */ return; } /* Report change to HAL if needed */ if (mSarInfo.isWifiScanOnlyEnabled != newIsEnabled) { mSarInfo.isWifiScanOnlyEnabled = newIsEnabled; updateSarScenario(); } } /** * Report Cell state event */ private void onCellStateChangeEvent(int state) { boolean newIsVoiceCall; switch (state) { case CALL_STATE_OFFHOOK: case CALL_STATE_RINGING: newIsVoiceCall = true; break; case CALL_STATE_IDLE: newIsVoiceCall = false; break; default: Log.e(TAG, "Invalid Cell State: " + state); return; } /* Report change to HAL if needed */ if (mSarInfo.isVoiceCall != newIsVoiceCall) { mSarInfo.isVoiceCall = newIsVoiceCall; if (mVerboseLoggingEnabled) { Log.d(TAG, "Voice Call = " + newIsVoiceCall); } updateSarScenario(); } } /** * Enable/disable verbose logging. */ public void enableVerboseLogging(boolean verboseEnabled) { mVerboseLoggingEnabled = verboseEnabled; } /** * dump() * Dumps SarManager state (as well as its SarInfo member variable state) */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Dump of SarManager"); pw.println("isSarSupported: " + mSupportSarTxPowerLimit); pw.println("isSarVoiceCallSupported: " + mSupportSarVoiceCall); pw.println("isSarSoftApSupported: " + mSupportSarSoftAp); pw.println(""); if (mSarInfo != null) { mSarInfo.dump(fd, pw, args); } } /** * Listen for phone call state events to set/reset TX power limits for SAR requirements. */ private class WifiPhoneStateListener extends PhoneStateListener { WifiPhoneStateListener(Looper looper) { super(new HandlerExecutor(new Handler(looper))); } /** * onCallStateChanged() * This callback is called when a call state event is received * Note that this runs in the WifiCoreHandlerThread * since the corresponding Looper was passed to the WifiPhoneStateListener constructor. */ @Override public void onCallStateChanged(int state, String incomingNumber) { Log.d(TAG, "Received Phone State Change: " + state); /* In case of an unsolicited event */ if (!mSupportSarTxPowerLimit || !mSupportSarVoiceCall) { return; } onCellStateChangeEvent(state); } } /** * updateSarScenario() * Update HAL with the new SAR scenario if needed. */ private void updateSarScenario() { if (!mSarInfo.shouldReport()) { return; } /* Report info to HAL*/ if (mWifiNative.selectTxPowerScenario(mSarInfo)) { mSarInfo.reportingSuccessful(); } else { Log.e(TAG, "Failed in WifiNative.selectTxPowerScenario()"); } return; } }