FileDocCategorySizeDatePackage
NetworkControllerImpl.javaAPI DocAndroid 5.1 API78893Thu Mar 12 22:22:42 GMT 2015com.android.systemui.statusbar.policy

NetworkControllerImpl.java

/*
 * 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<Integer, MobileSignalController> mMobileSignalControllers =
            new HashMap<Integer, MobileSignalController>();
    // 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<SubscriptionInfo> mCurrentSubscriptions
            = new ArrayList<SubscriptionInfo>();

    // All the callbacks.
    private ArrayList<EmergencyListener> mEmergencyListeners = new ArrayList<EmergencyListener>();
    private ArrayList<CarrierLabelListener> mCarrierListeners =
            new ArrayList<CarrierLabelListener>();
    private ArrayList<SignalCluster> mSignalClusters = new ArrayList<SignalCluster>();
    private ArrayList<NetworkSignalChangedCallback> mSignalsChangedCallbacks =
            new ArrayList<NetworkSignalChangedCallback>();
    @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<Void, Void, Void>() {
            @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<SubscriptionInfo> 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<SubscriptionInfo> subscriptions) {
        Collections.sort(subscriptions, new Comparator<SubscriptionInfo>() {
            @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<Integer, MobileSignalController> cachedControllers =
                new HashMap<Integer, MobileSignalController>(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<SubscriptionInfo> 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<SubscriptionInfo> subs = new ArrayList<SubscriptionInfo>();
                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<WifiSignalController.WifiState, SignalController.IconGroup> {
        private final WifiManager mWifiManager;
        private final AsyncChannel mWifiChannel;
        private final boolean mHasMobileData;

        public WifiSignalController(Context context, boolean hasMobileData,
                List<NetworkSignalChangedCallback> signalCallbacks,
                List<SignalCluster> 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<WifiConfiguration> 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<MobileIconGroup> 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<NetworkSignalChangedCallback> signalCallbacks,
                List<SignalCluster> 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<T extends SignalController.State,
            I extends SignalController.IconGroup> {
        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<NetworkSignalChangedCallback> mSignalsChangedCallbacks;
        protected final List<SignalCluster> 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<NetworkSignalChangedCallback> signalCallbacks,
                List<SignalCluster> 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<SubscriptionInfo> 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;
        }
    }
}