/* * Copyright (C) 2010 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.systemui.statusbar.policy; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.provider.Settings; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.IccCardConstants; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.cdma.EriInfo; import com.android.internal.util.AsyncChannel; import com.android.systemui.DemoMode; import com.android.systemui.R; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; /** Platform implementation of the network controller. **/ public class NetworkControllerImpl extends BroadcastReceiver implements NetworkController, DemoMode { // debug static final String TAG = "NetworkController"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); // additional diagnostics, but not logspew static final boolean CHATTY = Log.isLoggable(TAG + ".Chat", Log.DEBUG); // Save the previous SignalController.States of all SignalControllers for dumps. static final boolean RECORD_HISTORY = true; // If RECORD_HISTORY how many to save, must be a power of 2. static final int HISTORY_SIZE = 16; private static final int INET_CONDITION_THRESHOLD = 50; private final Context mContext; private final TelephonyManager mPhone; private final WifiManager mWifiManager; private final ConnectivityManager mConnectivityManager; private final SubscriptionManager mSubscriptionManager; private final boolean mHasMobileDataFeature; private Config mConfig; // Subcontrollers. @VisibleForTesting final WifiSignalController mWifiSignalController; @VisibleForTesting final Map mMobileSignalControllers = new HashMap(); // When no SIMs are around at setup, and one is added later, it seems to default to the first // SIM for most actions. This may be null if there aren't any SIMs around. private MobileSignalController mDefaultSignalController; private final AccessPointControllerImpl mAccessPoints; private final MobileDataControllerImpl mMobileDataController; // Network types that replace the carrier label if the device does not support mobile data. private boolean mBluetoothTethered = false; private boolean mEthernetConnected = false; // state of inet connection private boolean mConnected = false; private boolean mInetCondition; // Used for Logging and demo. // BitSets indicating which network transport types (e.g., TRANSPORT_WIFI, TRANSPORT_MOBILE) are // connected and validated, respectively. private final BitSet mConnectedTransports = new BitSet(); private final BitSet mValidatedTransports = new BitSet(); // States that don't belong to a subcontroller. private boolean mAirplaneMode = false; private boolean mHasNoSims; private Locale mLocale = null; // This list holds our ordering. private List mCurrentSubscriptions = new ArrayList(); // All the callbacks. private ArrayList mEmergencyListeners = new ArrayList(); private ArrayList mCarrierListeners = new ArrayList(); private ArrayList mSignalClusters = new ArrayList(); private ArrayList mSignalsChangedCallbacks = new ArrayList(); @VisibleForTesting boolean mListening; // The current user ID. private int mCurrentUserId; /** * Construct this controller object and register for updates. */ public NetworkControllerImpl(Context context) { this(context, (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE), (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), (WifiManager) context.getSystemService(Context.WIFI_SERVICE), SubscriptionManager.from(context), Config.readConfig(context), new AccessPointControllerImpl(context), new MobileDataControllerImpl(context)); registerListeners(); } @VisibleForTesting NetworkControllerImpl(Context context, ConnectivityManager connectivityManager, TelephonyManager telephonyManager, WifiManager wifiManager, SubscriptionManager subManager, Config config, AccessPointControllerImpl accessPointController, MobileDataControllerImpl mobileDataController) { mContext = context; mConfig = config; mSubscriptionManager = subManager; mConnectivityManager = connectivityManager; mHasMobileDataFeature = mConnectivityManager.isNetworkSupported(ConnectivityManager.TYPE_MOBILE); // telephony mPhone = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); // wifi mWifiManager = wifiManager; mLocale = mContext.getResources().getConfiguration().locale; mAccessPoints = accessPointController; mMobileDataController = mobileDataController; mMobileDataController.setNetworkController(this); // TODO: Find a way to move this into MobileDataController. mMobileDataController.setCallback(new MobileDataControllerImpl.Callback() { @Override public void onMobileDataEnabled(boolean enabled) { notifyMobileDataEnabled(enabled); } }); mWifiSignalController = new WifiSignalController(mContext, mHasMobileDataFeature, mSignalsChangedCallbacks, mSignalClusters, this); // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it updateAirplaneMode(true /* force callback */); mAccessPoints.setNetworkController(this); } private void registerListeners() { for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.registerListener(); } mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener); // broadcasts IntentFilter filter = new IntentFilter(); filter.addAction(WifiManager.RSSI_CHANGED_ACTION); filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); filter.addAction(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED); filter.addAction(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE); filter.addAction(ConnectivityManager.INET_CONDITION_ACTION); filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); mContext.registerReceiver(this, filter); mListening = true; updateMobileControllers(); } private void unregisterListeners() { mListening = false; for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.unregisterListener(); } mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionListener); mContext.unregisterReceiver(this); } public int getConnectedWifiLevel() { return mWifiSignalController.getState().level; } @Override public AccessPointController getAccessPointController() { return mAccessPoints; } @Override public MobileDataController getMobileDataController() { return mMobileDataController; } public void addEmergencyListener(EmergencyListener listener) { mEmergencyListeners.add(listener); listener.setEmergencyCallsOnly(isEmergencyOnly()); } public void addCarrierLabel(CarrierLabelListener listener) { mCarrierListeners.add(listener); refreshCarrierLabel(); } private void notifyMobileDataEnabled(boolean enabled) { final int length = mSignalsChangedCallbacks.size(); for (int i = 0; i < length; i++) { mSignalsChangedCallbacks.get(i).onMobileDataEnabled(enabled); } } public boolean hasMobileDataFeature() { return mHasMobileDataFeature; } public boolean hasVoiceCallingFeature() { return mPhone.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE; } private MobileSignalController getDataController() { int dataSubId = SubscriptionManager.getDefaultDataSubId(); if (!SubscriptionManager.isValidSubscriptionId(dataSubId)) { if (DEBUG) Log.e(TAG, "No data sim selected"); return mDefaultSignalController; } if (mMobileSignalControllers.containsKey(dataSubId)) { return mMobileSignalControllers.get(dataSubId); } if (DEBUG) Log.e(TAG, "Cannot find controller for data sub: " + dataSubId); return mDefaultSignalController; } public String getMobileNetworkName() { MobileSignalController controller = getDataController(); return controller != null ? controller.getState().networkName : ""; } public boolean isEmergencyOnly() { int voiceSubId = SubscriptionManager.getDefaultVoiceSubId(); if (!SubscriptionManager.isValidSubscriptionId(voiceSubId)) { for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { if (!mobileSignalController.isEmergencyOnly()) { return false; } } } if (mMobileSignalControllers.containsKey(voiceSubId)) { return mMobileSignalControllers.get(voiceSubId).isEmergencyOnly(); } if (DEBUG) Log.e(TAG, "Cannot find controller for voice sub: " + voiceSubId); // Something is wrong, better assume we can't make calls... return true; } /** * Emergency status may have changed (triggered by MobileSignalController), * so we should recheck and send out the state to listeners. */ void recalculateEmergency() { final boolean emergencyOnly = isEmergencyOnly(); final int length = mEmergencyListeners.size(); for (int i = 0; i < length; i++) { mEmergencyListeners.get(i).setEmergencyCallsOnly(emergencyOnly); } // If the emergency has a chance to change, then so does the carrier // label. refreshCarrierLabel(); } public void addSignalCluster(SignalCluster cluster) { mSignalClusters.add(cluster); cluster.setSubs(mCurrentSubscriptions); cluster.setIsAirplaneMode(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode); cluster.setNoSims(mHasNoSims); mWifiSignalController.notifyListeners(); for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.notifyListeners(); } } public void addNetworkSignalChangedCallback(NetworkSignalChangedCallback cb) { mSignalsChangedCallbacks.add(cb); cb.onAirplaneModeChanged(mAirplaneMode); cb.onNoSimVisibleChanged(mHasNoSims); mWifiSignalController.notifyListeners(); for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.notifyListeners(); } } public void removeNetworkSignalChangedCallback(NetworkSignalChangedCallback cb) { mSignalsChangedCallbacks.remove(cb); } @Override public void setWifiEnabled(final boolean enabled) { new AsyncTask() { @Override protected Void doInBackground(Void... args) { // Disable tethering if enabling Wifi final int wifiApState = mWifiManager.getWifiApState(); if (enabled && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { mWifiManager.setWifiApEnabled(null, false); } mWifiManager.setWifiEnabled(enabled); return null; } }.execute(); } @Override public void onUserSwitched(int newUserId) { mCurrentUserId = newUserId; mAccessPoints.onUserSwitched(newUserId); updateConnectivity(); refreshCarrierLabel(); } @Override public void onReceive(Context context, Intent intent) { if (CHATTY) { Log.d(TAG, "onReceive: intent=" + intent); } final String action = intent.getAction(); if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE) || action.equals(ConnectivityManager.INET_CONDITION_ACTION)) { updateConnectivity(); refreshCarrierLabel(); } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { mConfig = Config.readConfig(mContext); handleConfigurationChanged(); } else if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { refreshLocale(); updateAirplaneMode(false); refreshCarrierLabel(); } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)) { // We are using different subs now, we might be able to make calls. recalculateEmergency(); } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { // Notify every MobileSignalController so they can know whether they are the // data sim or not. for (MobileSignalController controller : mMobileSignalControllers.values()) { controller.handleBroadcast(intent); } } else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) { // Might have different subscriptions now. updateMobileControllers(); } else { int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, SubscriptionManager.INVALID_SUBSCRIPTION_ID); if (SubscriptionManager.isValidSubscriptionId(subId)) { if (mMobileSignalControllers.containsKey(subId)) { mMobileSignalControllers.get(subId).handleBroadcast(intent); } else { // Can't find this subscription... We must be out of date. updateMobileControllers(); } } else { // No sub id, must be for the wifi. mWifiSignalController.handleBroadcast(intent); } } } @VisibleForTesting void handleConfigurationChanged() { for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.setConfiguration(mConfig); } refreshLocale(); refreshCarrierLabel(); } private void updateMobileControllers() { if (!mListening) { return; } List subscriptions = mSubscriptionManager.getActiveSubscriptionInfoList(); if (subscriptions == null) { subscriptions = Collections.emptyList(); } // If there have been no relevant changes to any of the subscriptions, we can leave as is. if (hasCorrectMobileControllers(subscriptions)) { // Even if the controllers are correct, make sure we have the right no sims state. // Such as on boot, don't need any controllers, because there are no sims, // but we still need to update the no sim state. updateNoSims(); return; } setCurrentSubscriptions(subscriptions); updateNoSims(); } @VisibleForTesting protected void updateNoSims() { boolean hasNoSims = mHasMobileDataFeature && mMobileSignalControllers.size() == 0; if (hasNoSims != mHasNoSims) { mHasNoSims = hasNoSims; notifyListeners(); } } @VisibleForTesting void setCurrentSubscriptions(List subscriptions) { Collections.sort(subscriptions, new Comparator() { @Override public int compare(SubscriptionInfo lhs, SubscriptionInfo rhs) { return lhs.getSimSlotIndex() == rhs.getSimSlotIndex() ? lhs.getSubscriptionId() - rhs.getSubscriptionId() : lhs.getSimSlotIndex() - rhs.getSimSlotIndex(); } }); final int length = mSignalClusters.size(); for (int i = 0; i < length; i++) { mSignalClusters.get(i).setSubs(subscriptions); } mCurrentSubscriptions = subscriptions; HashMap cachedControllers = new HashMap(mMobileSignalControllers); mMobileSignalControllers.clear(); final int num = subscriptions.size(); for (int i = 0; i < num; i++) { int subId = subscriptions.get(i).getSubscriptionId(); // If we have a copy of this controller already reuse it, otherwise make a new one. if (cachedControllers.containsKey(subId)) { mMobileSignalControllers.put(subId, cachedControllers.remove(subId)); } else { MobileSignalController controller = new MobileSignalController(mContext, mConfig, mHasMobileDataFeature, mPhone, mSignalsChangedCallbacks, mSignalClusters, this, subscriptions.get(i)); mMobileSignalControllers.put(subId, controller); if (subscriptions.get(i).getSimSlotIndex() == 0) { mDefaultSignalController = controller; } if (mListening) { controller.registerListener(); } } } if (mListening) { for (Integer key : cachedControllers.keySet()) { if (cachedControllers.get(key) == mDefaultSignalController) { mDefaultSignalController = null; } cachedControllers.get(key).unregisterListener(); } } // There may be new MobileSignalControllers around, make sure they get the current // inet condition and airplane mode. pushConnectivityToSignals(); updateAirplaneMode(true /* force */); } @VisibleForTesting boolean hasCorrectMobileControllers(List allSubscriptions) { if (allSubscriptions.size() != mMobileSignalControllers.size()) { return false; } for (SubscriptionInfo info : allSubscriptions) { if (!mMobileSignalControllers.containsKey(info.getSubscriptionId())) { return false; } } return true; } private void updateAirplaneMode(boolean force) { boolean airplaneMode = (Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 1); if (airplaneMode != mAirplaneMode || force) { mAirplaneMode = airplaneMode; for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.setAirplaneMode(mAirplaneMode); } notifyListeners(); refreshCarrierLabel(); } } private void refreshLocale() { Locale current = mContext.getResources().getConfiguration().locale; if (!current.equals(mLocale)) { mLocale = current; notifyAllListeners(); } } /** * Forces update of all callbacks on both SignalClusters and * NetworkSignalChangedCallbacks. */ private void notifyAllListeners() { notifyListeners(); for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.notifyListeners(); } mWifiSignalController.notifyListeners(); } /** * Notifies listeners of changes in state of to the NetworkController, but * does not notify for any info on SignalControllers, for that call * notifyAllListeners. */ private void notifyListeners() { int length = mSignalClusters.size(); for (int i = 0; i < length; i++) { mSignalClusters.get(i).setIsAirplaneMode(mAirplaneMode, TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode); mSignalClusters.get(i).setNoSims(mHasNoSims); } int signalsChangedLength = mSignalsChangedCallbacks.size(); for (int i = 0; i < signalsChangedLength; i++) { mSignalsChangedCallbacks.get(i).onAirplaneModeChanged(mAirplaneMode); mSignalsChangedCallbacks.get(i).onNoSimVisibleChanged(mHasNoSims); } } /** * Update the Inet conditions and what network we are connected to. */ private void updateConnectivity() { mConnectedTransports.clear(); mValidatedTransports.clear(); for (NetworkCapabilities nc : mConnectivityManager.getDefaultNetworkCapabilitiesForUser(mCurrentUserId)) { for (int transportType : nc.getTransportTypes()) { mConnectedTransports.set(transportType); if (nc.hasCapability(NET_CAPABILITY_VALIDATED)) { mValidatedTransports.set(transportType); } } } if (CHATTY) { Log.d(TAG, "updateConnectivity: mConnectedTransports=" + mConnectedTransports); Log.d(TAG, "updateConnectivity: mValidatedTransports=" + mValidatedTransports); } mConnected = !mConnectedTransports.isEmpty(); mInetCondition = !mValidatedTransports.isEmpty(); mBluetoothTethered = mConnectedTransports.get(TRANSPORT_BLUETOOTH); mEthernetConnected = mConnectedTransports.get(TRANSPORT_ETHERNET); pushConnectivityToSignals(); } /** * Pushes the current connectivity state to all SignalControllers. */ private void pushConnectivityToSignals() { // We want to update all the icons, all at once, for any condition change for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.setInetCondition( mInetCondition ? 1 : 0, mValidatedTransports.get(mobileSignalController.getTransportType()) ? 1 : 0); } mWifiSignalController.setInetCondition( mValidatedTransports.get(mWifiSignalController.getTransportType()) ? 1 : 0); } /** * Recalculate and update the carrier label. */ void refreshCarrierLabel() { Context context = mContext; WifiSignalController.WifiState wifiState = mWifiSignalController.getState(); String label = ""; for (MobileSignalController controller : mMobileSignalControllers.values()) { label = controller.getLabel(label, mConnected, mHasMobileDataFeature); } // TODO Simplify this ugliness, some of the flows below shouldn't be possible anymore // but stay for the sake of history. if (mBluetoothTethered && !mHasMobileDataFeature) { label = mContext.getString(R.string.bluetooth_tethered); } if (mEthernetConnected && !mHasMobileDataFeature) { label = context.getString(R.string.ethernet_label); } if (mAirplaneMode && !isEmergencyOnly()) { // combined values from connected wifi take precedence over airplane mode if (wifiState.connected && mHasMobileDataFeature) { // Suppress "No internet connection." from mobile if wifi connected. label = ""; } else { if (!mHasMobileDataFeature) { label = context.getString( R.string.status_bar_settings_signal_meter_disconnected); } } } else if (!isMobileDataConnected() && !wifiState.connected && !mBluetoothTethered && !mEthernetConnected && !mHasMobileDataFeature) { // Pretty much no connection. label = context.getString(R.string.status_bar_settings_signal_meter_disconnected); } // for mobile devices, we always show mobile connection info here (SPN/PLMN) // for other devices, we show whatever network is connected // This is determined above by references to mHasMobileDataFeature. int length = mCarrierListeners.size(); for (int i = 0; i < length; i++) { mCarrierListeners.get(i).setCarrierLabel(label); } } private boolean isMobileDataConnected() { MobileSignalController controller = getDataController(); return controller != null ? controller.getState().dataConnected : false; } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("NetworkController state:"); pw.println(" - telephony ------"); pw.print(" hasVoiceCallingFeature()="); pw.println(hasVoiceCallingFeature()); pw.println(" - Bluetooth ----"); pw.print(" mBtReverseTethered="); pw.println(mBluetoothTethered); pw.println(" - connectivity ------"); pw.print(" mConnectedTransports="); pw.println(mConnectedTransports); pw.print(" mValidatedTransports="); pw.println(mValidatedTransports); pw.print(" mInetCondition="); pw.println(mInetCondition); pw.print(" mAirplaneMode="); pw.println(mAirplaneMode); pw.print(" mLocale="); pw.println(mLocale); for (MobileSignalController mobileSignalController : mMobileSignalControllers.values()) { mobileSignalController.dump(pw); } mWifiSignalController.dump(pw); } private boolean mDemoMode; private int mDemoInetCondition; private WifiSignalController.WifiState mDemoWifiState; @Override public void dispatchDemoCommand(String command, Bundle args) { if (!mDemoMode && command.equals(COMMAND_ENTER)) { if (DEBUG) Log.d(TAG, "Entering demo mode"); unregisterListeners(); mDemoMode = true; mDemoInetCondition = mInetCondition ? 1 : 0; mDemoWifiState = mWifiSignalController.getState(); } else if (mDemoMode && command.equals(COMMAND_EXIT)) { if (DEBUG) Log.d(TAG, "Exiting demo mode"); mDemoMode = false; // Update what MobileSignalControllers, because they may change // to set the number of sim slots. updateMobileControllers(); for (MobileSignalController controller : mMobileSignalControllers.values()) { controller.resetLastState(); } mWifiSignalController.resetLastState(); registerListeners(); notifyAllListeners(); refreshCarrierLabel(); } else if (mDemoMode && command.equals(COMMAND_NETWORK)) { String airplane = args.getString("airplane"); if (airplane != null) { boolean show = airplane.equals("show"); int length = mSignalClusters.size(); for (int i = 0; i < length; i++) { mSignalClusters.get(i).setIsAirplaneMode(show, TelephonyIcons.FLIGHT_MODE_ICON, R.string.accessibility_airplane_mode); } } String fully = args.getString("fully"); if (fully != null) { mDemoInetCondition = Boolean.parseBoolean(fully) ? 1 : 0; mWifiSignalController.setInetCondition(mDemoInetCondition); for (MobileSignalController controller : mMobileSignalControllers.values()) { controller.setInetCondition(mDemoInetCondition, mDemoInetCondition); } } String wifi = args.getString("wifi"); if (wifi != null) { boolean show = wifi.equals("show"); String level = args.getString("level"); if (level != null) { mDemoWifiState.level = level.equals("null") ? -1 : Math.min(Integer.parseInt(level), WifiIcons.WIFI_LEVEL_COUNT - 1); mDemoWifiState.connected = mDemoWifiState.level >= 0; } mDemoWifiState.enabled = show; mWifiSignalController.notifyListeners(); } String sims = args.getString("sims"); if (sims != null) { int num = Integer.parseInt(sims); List subs = new ArrayList(); if (num != mMobileSignalControllers.size()) { mMobileSignalControllers.clear(); int start = mSubscriptionManager.getActiveSubscriptionInfoCountMax(); for (int i = start /* get out of normal index range */; i < start + num; i++) { SubscriptionInfo info = new SubscriptionInfo(i, "", i, "", "", 0, 0, "", 0, null, 0, 0, ""); subs.add(info); mMobileSignalControllers.put(i, new MobileSignalController(mContext, mConfig, mHasMobileDataFeature, mPhone, mSignalsChangedCallbacks, mSignalClusters, this, info)); } } final int n = mSignalClusters.size(); for (int i = 0; i < n; i++) { mSignalClusters.get(i).setSubs(subs); } } String nosim = args.getString("nosim"); if (nosim != null) { boolean show = nosim.equals("show"); final int n = mSignalClusters.size(); for (int i = 0; i < n; i++) { mSignalClusters.get(i).setNoSims(show); } } String mobile = args.getString("mobile"); if (mobile != null) { boolean show = mobile.equals("show"); String datatype = args.getString("datatype"); String slotString = args.getString("slot"); int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString); // Hack to index linearly for easy use. MobileSignalController controller = mMobileSignalControllers .values().toArray(new MobileSignalController[0])[slot]; controller.getState().dataSim = datatype != null; if (datatype != null) { controller.getState().iconGroup = datatype.equals("1x") ? TelephonyIcons.ONE_X : datatype.equals("3g") ? TelephonyIcons.THREE_G : datatype.equals("4g") ? TelephonyIcons.FOUR_G : datatype.equals("e") ? TelephonyIcons.E : datatype.equals("g") ? TelephonyIcons.G : datatype.equals("h") ? TelephonyIcons.H : datatype.equals("lte") ? TelephonyIcons.LTE : datatype.equals("roam") ? TelephonyIcons.ROAMING : TelephonyIcons.UNKNOWN; } int[][] icons = TelephonyIcons.TELEPHONY_SIGNAL_STRENGTH; String level = args.getString("level"); if (level != null) { controller.getState().level = level.equals("null") ? -1 : Math.min(Integer.parseInt(level), icons[0].length - 1); controller.getState().connected = controller.getState().level >= 0; } controller.getState().enabled = show; controller.notifyListeners(); } refreshCarrierLabel(); } } private final OnSubscriptionsChangedListener mSubscriptionListener = new OnSubscriptionsChangedListener() { @Override public void onSubscriptionsChanged() { updateMobileControllers(); }; }; // TODO: Move to its own file. static class WifiSignalController extends SignalController { private final WifiManager mWifiManager; private final AsyncChannel mWifiChannel; private final boolean mHasMobileData; public WifiSignalController(Context context, boolean hasMobileData, List signalCallbacks, List signalClusters, NetworkControllerImpl networkController) { super("WifiSignalController", context, NetworkCapabilities.TRANSPORT_WIFI, signalCallbacks, signalClusters, networkController); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mHasMobileData = hasMobileData; Handler handler = new WifiHandler(); mWifiChannel = new AsyncChannel(); Messenger wifiMessenger = mWifiManager.getWifiServiceMessenger(); if (wifiMessenger != null) { mWifiChannel.connect(context, handler, wifiMessenger); } // WiFi only has one state. mCurrentState.iconGroup = mLastState.iconGroup = new IconGroup( "Wi-Fi Icons", WifiIcons.WIFI_SIGNAL_STRENGTH, WifiIcons.QS_WIFI_SIGNAL_STRENGTH, AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH, WifiIcons.WIFI_NO_NETWORK, WifiIcons.QS_WIFI_NO_NETWORK, WifiIcons.WIFI_NO_NETWORK, WifiIcons.QS_WIFI_NO_NETWORK, AccessibilityContentDescriptions.WIFI_NO_CONNECTION ); } @Override protected WifiState cleanState() { return new WifiState(); } @Override public void notifyListeners() { // only show wifi in the cluster if connected or if wifi-only boolean wifiVisible = mCurrentState.enabled && (mCurrentState.connected || !mHasMobileData); String wifiDesc = wifiVisible ? mCurrentState.ssid : null; boolean ssidPresent = wifiVisible && mCurrentState.ssid != null; String contentDescription = getStringIfExists(getContentDescription()); int length = mSignalsChangedCallbacks.size(); for (int i = 0; i < length; i++) { mSignalsChangedCallbacks.get(i).onWifiSignalChanged(mCurrentState.enabled, mCurrentState.connected, getQsCurrentIconId(), ssidPresent && mCurrentState.activityIn, ssidPresent && mCurrentState.activityOut, contentDescription, wifiDesc); } int signalClustersLength = mSignalClusters.size(); for (int i = 0; i < signalClustersLength; i++) { mSignalClusters.get(i).setWifiIndicators(wifiVisible, getCurrentIconId(), contentDescription); } } /** * Extract wifi state directly from broadcasts about changes in wifi state. */ public void handleBroadcast(Intent intent) { String action = intent.getAction(); if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { mCurrentState.enabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED; } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { final NetworkInfo networkInfo = (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); mCurrentState.connected = networkInfo != null && networkInfo.isConnected(); // If Connected grab the signal strength and ssid. if (mCurrentState.connected) { // try getting it out of the intent first WifiInfo info = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) != null ? (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) : mWifiManager.getConnectionInfo(); if (info != null) { mCurrentState.ssid = getSsid(info); } else { mCurrentState.ssid = null; } } else if (!mCurrentState.connected) { mCurrentState.ssid = null; } } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) { // Default to -200 as its below WifiManager.MIN_RSSI. mCurrentState.rssi = intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200); mCurrentState.level = WifiManager.calculateSignalLevel( mCurrentState.rssi, WifiIcons.WIFI_LEVEL_COUNT); } notifyListenersIfNecessary(); } private String getSsid(WifiInfo info) { String ssid = info.getSSID(); if (ssid != null) { return ssid; } // OK, it's not in the connectionInfo; we have to go hunting for it List networks = mWifiManager.getConfiguredNetworks(); int length = networks.size(); for (int i = 0; i < length; i++) { if (networks.get(i).networkId == info.getNetworkId()) { return networks.get(i).SSID; } } return null; } @VisibleForTesting void setActivity(int wifiActivity) { mCurrentState.activityIn = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT || wifiActivity == WifiManager.DATA_ACTIVITY_IN; mCurrentState.activityOut = wifiActivity == WifiManager.DATA_ACTIVITY_INOUT || wifiActivity == WifiManager.DATA_ACTIVITY_OUT; notifyListenersIfNecessary(); } /** * Handler to receive the data activity on wifi. */ class WifiHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { mWifiChannel.sendMessage(Message.obtain(this, AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)); } else { Log.e(mTag, "Failed to connect to wifi"); } break; case WifiManager.DATA_ACTIVITY_NOTIFICATION: setActivity(msg.arg1); break; default: // Ignore break; } } } static class WifiState extends SignalController.State { String ssid; @Override public void copyFrom(State s) { super.copyFrom(s); WifiState state = (WifiState) s; ssid = state.ssid; } @Override protected void toString(StringBuilder builder) { super.toString(builder); builder.append(',').append("ssid=").append(ssid); } @Override public boolean equals(Object o) { return super.equals(o) && Objects.equals(((WifiState) o).ssid, ssid); } } } // TODO: Move to its own file. public static class MobileSignalController extends SignalController< MobileSignalController.MobileState, MobileSignalController.MobileIconGroup> { private final TelephonyManager mPhone; private final String mNetworkNameDefault; private final String mNetworkNameSeparator; @VisibleForTesting final PhoneStateListener mPhoneStateListener; // Save entire info for logging, we only use the id. private final SubscriptionInfo mSubscriptionInfo; // @VisibleForDemoMode final SparseArray mNetworkToIconLookup; // Since some pieces of the phone state are interdependent we store it locally, // this could potentially become part of MobileState for simplification/complication // of code. private IccCardConstants.State mSimState = IccCardConstants.State.READY; private int mDataNetType = TelephonyManager.NETWORK_TYPE_UNKNOWN; private int mDataState = TelephonyManager.DATA_DISCONNECTED; private ServiceState mServiceState; private SignalStrength mSignalStrength; private MobileIconGroup mDefaultIcons; private Config mConfig; // TODO: Reduce number of vars passed in, if we have the NetworkController, probably don't // need listener lists anymore. public MobileSignalController(Context context, Config config, boolean hasMobileData, TelephonyManager phone, List signalCallbacks, List signalClusters, NetworkControllerImpl networkController, SubscriptionInfo info) { super("MobileSignalController(" + info.getSubscriptionId() + ")", context, NetworkCapabilities.TRANSPORT_CELLULAR, signalCallbacks, signalClusters, networkController); mNetworkToIconLookup = new SparseArray<>(); mConfig = config; mPhone = phone; mSubscriptionInfo = info; mPhoneStateListener = new MobilePhoneStateListener(info.getSubscriptionId()); mNetworkNameSeparator = getStringIfExists(R.string.status_bar_network_name_separator); mNetworkNameDefault = getStringIfExists( com.android.internal.R.string.lockscreen_carrier_default); mapIconSets(); mLastState.networkName = mCurrentState.networkName = mNetworkNameDefault; mLastState.enabled = mCurrentState.enabled = hasMobileData; mLastState.iconGroup = mCurrentState.iconGroup = mDefaultIcons; // Get initial data sim state. updateDataSim(); } public void setConfiguration(Config config) { mConfig = config; mapIconSets(); updateTelephony(); } /** * Get (the mobile parts of) the carrier string. * * @param currentLabel can be used for concatenation, currently just empty * @param connected whether the device has connection to the internet at all * @param isMobileLabel whether to always return the network or just when data is connected */ public String getLabel(String currentLabel, boolean connected, boolean isMobileLabel) { if (!mCurrentState.enabled) { return ""; } else { String mobileLabel = ""; // We want to show the carrier name if in service and either: // - We are connected to mobile data, or // - We are not connected to mobile data, as long as the *reason* packets are not // being routed over that link is that we have better connectivity via wifi. // If data is disconnected for some other reason but wifi (or ethernet/bluetooth) // is connected, we show nothing. // Otherwise (nothing connected) we show "No internet connection". if (mCurrentState.dataConnected) { mobileLabel = mCurrentState.networkName; } else if (connected || mCurrentState.isEmergency) { if (mCurrentState.connected || mCurrentState.isEmergency) { // The isEmergencyOnly test covers the case of a phone with no SIM mobileLabel = mCurrentState.networkName; } } else { mobileLabel = mContext.getString( R.string.status_bar_settings_signal_meter_disconnected); } if (currentLabel.length() != 0) { currentLabel = currentLabel + mNetworkNameSeparator; } // Now for things that should only be shown when actually using mobile data. if (isMobileLabel) { return currentLabel + mobileLabel; } else { return currentLabel + (mCurrentState.dataConnected ? mobileLabel : currentLabel); } } } public int getDataContentDescription() { return getIcons().mDataContentDescription; } public void setAirplaneMode(boolean airplaneMode) { mCurrentState.airplaneMode = airplaneMode; notifyListenersIfNecessary(); } public void setInetCondition(int inetCondition, int inetConditionForNetwork) { // For mobile data, use general inet condition for phone signal indexing, // and network specific for data indexing (I think this might be a bug, but // keeping for now). // TODO: Update with explanation of why. mCurrentState.inetForNetwork = inetConditionForNetwork; setInetCondition(inetCondition); } /** * Start listening for phone state changes. */ public void registerListener() { mPhone.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS | PhoneStateListener.LISTEN_CALL_STATE | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE | PhoneStateListener.LISTEN_DATA_ACTIVITY); } /** * Stop listening for phone state changes. */ public void unregisterListener() { mPhone.listen(mPhoneStateListener, 0); } /** * Produce a mapping of data network types to icon groups for simple and quick use in * updateTelephony. */ private void mapIconSets() { mNetworkToIconLookup.clear(); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_A, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EVDO_B, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EHRPD, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UMTS, TelephonyIcons.THREE_G); if (!mConfig.showAtLeast3G) { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, TelephonyIcons.UNKNOWN); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, TelephonyIcons.E); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, TelephonyIcons.ONE_X); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyIcons.ONE_X); mDefaultIcons = TelephonyIcons.G; } else { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_EDGE, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_CDMA, TelephonyIcons.THREE_G); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyIcons.THREE_G); mDefaultIcons = TelephonyIcons.THREE_G; } MobileIconGroup hGroup = TelephonyIcons.THREE_G; if (mConfig.hspaDataDistinguishable) { hGroup = TelephonyIcons.H; } mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSDPA, hGroup); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSUPA, hGroup); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPA, hGroup); mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_HSPAP, hGroup); if (mConfig.show4gForLte) { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.FOUR_G); } else { mNetworkToIconLookup.put(TelephonyManager.NETWORK_TYPE_LTE, TelephonyIcons.LTE); } } @Override public void notifyListeners() { MobileIconGroup icons = getIcons(); String contentDescription = getStringIfExists(getContentDescription()); String dataContentDescription = getStringIfExists(icons.mDataContentDescription); boolean showDataIcon = mCurrentState.dataConnected && mCurrentState.inetForNetwork != 0 || mCurrentState.iconGroup == TelephonyIcons.ROAMING; // Only send data sim callbacks to QS. if (mCurrentState.dataSim) { int qsTypeIcon = showDataIcon ? icons.mQsDataType[mCurrentState.inetForNetwork] : 0; int length = mSignalsChangedCallbacks.size(); for (int i = 0; i < length; i++) { mSignalsChangedCallbacks.get(i).onMobileDataSignalChanged(mCurrentState.enabled && !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription, qsTypeIcon, mCurrentState.dataConnected && mCurrentState.activityIn, mCurrentState.dataConnected && mCurrentState.activityOut, dataContentDescription, mCurrentState.isEmergency ? null : mCurrentState.networkName, // Only wide if actually showing something. icons.mIsWide && qsTypeIcon != 0); } } int typeIcon = showDataIcon ? icons.mDataType : 0; int signalClustersLength = mSignalClusters.size(); for (int i = 0; i < signalClustersLength; i++) { mSignalClusters.get(i).setMobileDataIndicators( mCurrentState.enabled && !mCurrentState.airplaneMode, getCurrentIconId(), typeIcon, contentDescription, dataContentDescription, // Only wide if actually showing something. icons.mIsWide && typeIcon != 0, mSubscriptionInfo.getSubscriptionId()); } } @Override protected MobileState cleanState() { return new MobileState(); } private boolean hasService() { if (mServiceState != null) { // Consider the device to be in service if either voice or data // service is available. Some SIM cards are marketed as data-only // and do not support voice service, and on these SIM cards, we // want to show signal bars for data service as well as the "no // service" or "emergency calls only" text that indicates that voice // is not available. switch (mServiceState.getVoiceRegState()) { case ServiceState.STATE_POWER_OFF: return false; case ServiceState.STATE_OUT_OF_SERVICE: case ServiceState.STATE_EMERGENCY_ONLY: return mServiceState.getDataRegState() == ServiceState.STATE_IN_SERVICE; default: return true; } } else { return false; } } private boolean isCdma() { return (mSignalStrength != null) && !mSignalStrength.isGsm(); } public boolean isEmergencyOnly() { return (mServiceState != null && mServiceState.isEmergencyOnly()); } private boolean isRoaming() { if (isCdma()) { final int iconMode = mServiceState.getCdmaEriIconMode(); return mServiceState.getCdmaEriIconIndex() != EriInfo.ROAMING_INDICATOR_OFF && (iconMode == EriInfo.ROAMING_ICON_MODE_NORMAL || iconMode == EriInfo.ROAMING_ICON_MODE_FLASH); } else { return mServiceState != null && mServiceState.getRoaming(); } } public void handleBroadcast(Intent intent) { String action = intent.getAction(); if (action.equals(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)) { updateNetworkName(intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false), intent.getStringExtra(TelephonyIntents.EXTRA_SPN), intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false), intent.getStringExtra(TelephonyIntents.EXTRA_PLMN)); notifyListenersIfNecessary(); } else if (action.equals(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) { updateDataSim(); } } private void updateDataSim() { int defaultDataSub = SubscriptionManager.getDefaultDataSubId(); if (SubscriptionManager.isValidSubscriptionId(defaultDataSub)) { mCurrentState.dataSim = defaultDataSub == mSubscriptionInfo.getSubscriptionId(); } else { // There doesn't seem to be a data sim selected, however if // there isn't a MobileSignalController with dataSim set, then // QS won't get any callbacks and will be blank. Instead // lets just assume we are the data sim (which will basically // show one at random) in QS until one is selected. The user // should pick one soon after, so we shouldn't be in this state // for long. mCurrentState.dataSim = true; } notifyListenersIfNecessary(); } /** * Updates the network's name based on incoming spn and plmn. */ void updateNetworkName(boolean showSpn, String spn, boolean showPlmn, String plmn) { if (CHATTY) { Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn + " spn=" + spn + " showPlmn=" + showPlmn + " plmn=" + plmn); } StringBuilder str = new StringBuilder(); if (showPlmn && plmn != null) { str.append(plmn); } if (showSpn && spn != null) { if (str.length() != 0) { str.append(mNetworkNameSeparator); } str.append(spn); } if (str.length() != 0) { mCurrentState.networkName = str.toString(); } else { mCurrentState.networkName = mNetworkNameDefault; } } /** * Updates the current state based on mServiceState, mSignalStrength, mDataNetType, * mDataState, and mSimState. It should be called any time one of these is updated. * This will call listeners if necessary. */ private final void updateTelephony() { if (DEBUG) { Log.d(TAG, "updateTelephonySignalStrength: hasService=" + hasService() + " ss=" + mSignalStrength); } mCurrentState.connected = hasService() && mSignalStrength != null; if (mCurrentState.connected) { if (!mSignalStrength.isGsm() && mConfig.alwaysShowCdmaRssi) { mCurrentState.level = mSignalStrength.getCdmaLevel(); } else { mCurrentState.level = mSignalStrength.getLevel(); } } if (mNetworkToIconLookup.indexOfKey(mDataNetType) >= 0) { mCurrentState.iconGroup = mNetworkToIconLookup.get(mDataNetType); } else { mCurrentState.iconGroup = mDefaultIcons; } mCurrentState.dataConnected = mCurrentState.connected && mDataState == TelephonyManager.DATA_CONNECTED; if (isRoaming()) { mCurrentState.iconGroup = TelephonyIcons.ROAMING; } if (isEmergencyOnly() != mCurrentState.isEmergency) { mCurrentState.isEmergency = isEmergencyOnly(); mNetworkController.recalculateEmergency(); } // Fill in the network name if we think we have it. if (mCurrentState.networkName == mNetworkNameDefault && mServiceState != null && mServiceState.getOperatorAlphaShort() != null) { mCurrentState.networkName = mServiceState.getOperatorAlphaShort(); } notifyListenersIfNecessary(); } @VisibleForTesting void setActivity(int activity) { mCurrentState.activityIn = activity == TelephonyManager.DATA_ACTIVITY_INOUT || activity == TelephonyManager.DATA_ACTIVITY_IN; mCurrentState.activityOut = activity == TelephonyManager.DATA_ACTIVITY_INOUT || activity == TelephonyManager.DATA_ACTIVITY_OUT; notifyListenersIfNecessary(); } @Override public void dump(PrintWriter pw) { super.dump(pw); pw.println(" mSubscription=" + mSubscriptionInfo + ","); pw.println(" mServiceState=" + mServiceState + ","); pw.println(" mSignalStrength=" + mSignalStrength + ","); pw.println(" mDataState=" + mDataState + ","); pw.println(" mDataNetType=" + mDataNetType + ","); } class MobilePhoneStateListener extends PhoneStateListener { public MobilePhoneStateListener(int subId) { super(subId); } @Override public void onSignalStrengthsChanged(SignalStrength signalStrength) { if (DEBUG) { Log.d(mTag, "onSignalStrengthsChanged signalStrength=" + signalStrength + ((signalStrength == null) ? "" : (" level=" + signalStrength.getLevel()))); } mSignalStrength = signalStrength; updateTelephony(); } @Override public void onServiceStateChanged(ServiceState state) { if (DEBUG) { Log.d(mTag, "onServiceStateChanged voiceState=" + state.getVoiceRegState() + " dataState=" + state.getDataRegState()); } mServiceState = state; updateTelephony(); } @Override public void onDataConnectionStateChanged(int state, int networkType) { if (DEBUG) { Log.d(mTag, "onDataConnectionStateChanged: state=" + state + " type=" + networkType); } mDataState = state; mDataNetType = networkType; updateTelephony(); } @Override public void onDataActivity(int direction) { if (DEBUG) { Log.d(mTag, "onDataActivity: direction=" + direction); } setActivity(direction); } }; static class MobileIconGroup extends SignalController.IconGroup { final int mDataContentDescription; // mContentDescriptionDataType final int mDataType; final boolean mIsWide; final int[] mQsDataType; public MobileIconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, int discContentDesc, int dataContentDesc, int dataType, boolean isWide, int[] qsDataType) { super(name, sbIcons, qsIcons, contentDesc, sbNullState, qsNullState, sbDiscState, qsDiscState, discContentDesc); mDataContentDescription = dataContentDesc; mDataType = dataType; mIsWide = isWide; mQsDataType = qsDataType; } } static class MobileState extends SignalController.State { String networkName; boolean dataSim; boolean dataConnected; boolean isEmergency; boolean airplaneMode; int inetForNetwork; @Override public void copyFrom(State s) { super.copyFrom(s); MobileState state = (MobileState) s; dataSim = state.dataSim; networkName = state.networkName; dataConnected = state.dataConnected; inetForNetwork = state.inetForNetwork; isEmergency = state.isEmergency; airplaneMode = state.airplaneMode; } @Override protected void toString(StringBuilder builder) { super.toString(builder); builder.append(','); builder.append("dataSim=").append(dataSim).append(','); builder.append("networkName=").append(networkName).append(','); builder.append("dataConnected=").append(dataConnected).append(','); builder.append("inetForNetwork=").append(inetForNetwork).append(','); builder.append("isEmergency=").append(isEmergency).append(','); builder.append("airplaneMode=").append(airplaneMode); } @Override public boolean equals(Object o) { return super.equals(o) && Objects.equals(((MobileState) o).networkName, networkName) && ((MobileState) o).dataSim == dataSim && ((MobileState) o).dataConnected == dataConnected && ((MobileState) o).isEmergency == isEmergency && ((MobileState) o).airplaneMode == airplaneMode && ((MobileState) o).inetForNetwork == inetForNetwork; } } } /** * Common base class for handling signal for both wifi and mobile data. */ static abstract class SignalController { protected final String mTag; protected final T mCurrentState; protected final T mLastState; protected final int mTransportType; protected final Context mContext; // The owner of the SignalController (i.e. NetworkController will maintain the following // lists and call notifyListeners whenever the list has changed to ensure everyone // is aware of current state. protected final List mSignalsChangedCallbacks; protected final List mSignalClusters; protected final NetworkControllerImpl mNetworkController; // Save the previous HISTORY_SIZE states for logging. private final State[] mHistory; // Where to copy the next state into. private int mHistoryIndex; public SignalController(String tag, Context context, int type, List signalCallbacks, List signalClusters, NetworkControllerImpl networkController) { mTag = TAG + "." + tag; mNetworkController = networkController; mTransportType = type; mContext = context; mSignalsChangedCallbacks = signalCallbacks; mSignalClusters = signalClusters; mCurrentState = cleanState(); mLastState = cleanState(); if (RECORD_HISTORY) { mHistory = new State[HISTORY_SIZE]; for (int i = 0; i < HISTORY_SIZE; i++) { mHistory[i] = cleanState(); } } } public T getState() { return mCurrentState; } public int getTransportType() { return mTransportType; } public void setInetCondition(int inetCondition) { mCurrentState.inetCondition = inetCondition; notifyListenersIfNecessary(); } /** * Used at the end of demo mode to clear out any ugly state that it has created. * Since we haven't had any callbacks, then isDirty will not have been triggered, * so we can just take the last good state directly from there. * * Used for demo mode. */ void resetLastState() { mCurrentState.copyFrom(mLastState); } /** * Determines if the state of this signal controller has changed and * needs to trigger callbacks related to it. */ public boolean isDirty() { if (!mLastState.equals(mCurrentState)) { if (DEBUG) { Log.d(mTag, "Change in state from: " + mLastState + "\n" + "\tto: " + mCurrentState); } return true; } return false; } public void saveLastState() { if (RECORD_HISTORY) { recordLastState(); } // Updates the current time. mCurrentState.time = System.currentTimeMillis(); mLastState.copyFrom(mCurrentState); } /** * Gets the signal icon for QS based on current state of connected, enabled, and level. */ public int getQsCurrentIconId() { if (mCurrentState.connected) { return getIcons().mQsIcons[mCurrentState.inetCondition][mCurrentState.level]; } else if (mCurrentState.enabled) { return getIcons().mQsDiscState; } else { return getIcons().mQsNullState; } } /** * Gets the signal icon for SB based on current state of connected, enabled, and level. */ public int getCurrentIconId() { if (mCurrentState.connected) { return getIcons().mSbIcons[mCurrentState.inetCondition][mCurrentState.level]; } else if (mCurrentState.enabled) { return getIcons().mSbDiscState; } else { return getIcons().mSbNullState; } } /** * Gets the content description id for the signal based on current state of connected and * level. */ public int getContentDescription() { if (mCurrentState.connected) { return getIcons().mContentDesc[mCurrentState.level]; } else { return getIcons().mDiscContentDesc; } } public void notifyListenersIfNecessary() { if (isDirty()) { saveLastState(); notifyListeners(); mNetworkController.refreshCarrierLabel(); } } /** * Returns the resource if resId is not 0, and an empty string otherwise. */ protected String getStringIfExists(int resId) { return resId != 0 ? mContext.getString(resId) : ""; } protected I getIcons() { return (I) mCurrentState.iconGroup; } /** * Saves the last state of any changes, so we can log the current * and last value of any state data. */ protected void recordLastState() { mHistory[mHistoryIndex++ & (HISTORY_SIZE - 1)].copyFrom(mLastState); } public void dump(PrintWriter pw) { pw.println(" - " + mTag + " -----"); pw.println(" Current State: " + mCurrentState); if (RECORD_HISTORY) { // Count up the states that actually contain time stamps, and only display those. int size = 0; for (int i = 0; i < HISTORY_SIZE; i++) { if (mHistory[i].time != 0) size++; } // Print out the previous states in ordered number. for (int i = mHistoryIndex + HISTORY_SIZE - 1; i >= mHistoryIndex + HISTORY_SIZE - size; i--) { pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + ": " + mHistory[i & (HISTORY_SIZE - 1)]); } } } /** * Trigger callbacks based on current state. The callbacks should be completely * based on current state, and only need to be called in the scenario where * mCurrentState != mLastState. */ public abstract void notifyListeners(); /** * Generate a blank T. */ protected abstract T cleanState(); /* * Holds icons for a given state. Arrays are generally indexed as inet * state (full connectivity or not) first, and second dimension as * signal strength. */ static class IconGroup { final int[][] mSbIcons; final int[][] mQsIcons; final int[] mContentDesc; final int mSbNullState; final int mQsNullState; final int mSbDiscState; final int mQsDiscState; final int mDiscContentDesc; // For logging. final String mName; public IconGroup(String name, int[][] sbIcons, int[][] qsIcons, int[] contentDesc, int sbNullState, int qsNullState, int sbDiscState, int qsDiscState, int discContentDesc) { mName = name; mSbIcons = sbIcons; mQsIcons = qsIcons; mContentDesc = contentDesc; mSbNullState = sbNullState; mQsNullState = qsNullState; mSbDiscState = sbDiscState; mQsDiscState = qsDiscState; mDiscContentDesc = discContentDesc; } @Override public String toString() { return "IconGroup(" + mName + ")"; } } static class State { boolean connected; boolean enabled; boolean activityIn; boolean activityOut; int level; IconGroup iconGroup; int inetCondition; int rssi; // Only for logging. // Not used for comparison, just used for logging. long time; public void copyFrom(State state) { connected = state.connected; enabled = state.enabled; level = state.level; iconGroup = state.iconGroup; inetCondition = state.inetCondition; activityIn = state.activityIn; activityOut = state.activityOut; rssi = state.rssi; time = state.time; } @Override public String toString() { if (time != 0) { StringBuilder builder = new StringBuilder(); toString(builder); return builder.toString(); } else { return "Empty " + getClass().getSimpleName(); } } protected void toString(StringBuilder builder) { builder.append("connected=").append(connected).append(',') .append("enabled=").append(enabled).append(',') .append("level=").append(level).append(',') .append("inetCondition=").append(inetCondition).append(',') .append("iconGroup=").append(iconGroup).append(',') .append("activityIn=").append(activityIn).append(',') .append("activityOut=").append(activityOut).append(',') .append("rssi=").append(rssi).append(',') .append("lastModified=").append(DateFormat.format("MM-dd hh:mm:ss", time)); } @Override public boolean equals(Object o) { if (!o.getClass().equals(getClass())) { return false; } State other = (State) o; return other.connected == connected && other.enabled == enabled && other.level == level && other.inetCondition == inetCondition && other.iconGroup == iconGroup && other.activityIn == activityIn && other.activityOut == activityOut && other.rssi == rssi; } } } public interface SignalCluster { void setWifiIndicators(boolean visible, int strengthIcon, String contentDescription); void setMobileDataIndicators(boolean visible, int strengthIcon, int typeIcon, String contentDescription, String typeContentDescription, boolean isTypeIconWide, int subId); void setSubs(List subs); void setNoSims(boolean show); void setIsAirplaneMode(boolean is, int airplaneIcon, int contentDescription); } public interface EmergencyListener { void setEmergencyCallsOnly(boolean emergencyOnly); } public interface CarrierLabelListener { void setCarrierLabel(String label); } @VisibleForTesting static class Config { boolean showAtLeast3G = false; boolean alwaysShowCdmaRssi = false; boolean show4gForLte = false; boolean hspaDataDistinguishable; static Config readConfig(Context context) { Config config = new Config(); Resources res = context.getResources(); config.showAtLeast3G = res.getBoolean(R.bool.config_showMin3G); config.alwaysShowCdmaRssi = res.getBoolean(com.android.internal.R.bool.config_alwaysUseCdmaRssi); config.show4gForLte = res.getBoolean(R.bool.config_show4GForLTE); config.hspaDataDistinguishable = res.getBoolean(R.bool.config_hspa_data_distinguishable); return config; } } }