com.vonglasow.michael.satstat.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.vonglasow.michael.satstat.MainActivity.java

Source

/*
 * Copyright  2013 Michael von Glasow.
 * 
 * This file is part of LSRN Tools.
 *
 * LSRN Tools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LSRN Tools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LSRN Tools.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.vonglasow.michael.satstat;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.app.ActionBar.Tab;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import static android.hardware.SensorManager.SENSOR_STATUS_ACCURACY_HIGH;
import static android.hardware.SensorManager.SENSOR_STATUS_ACCURACY_LOW;
import static android.hardware.SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM;
import static android.hardware.SensorManager.SENSOR_STATUS_UNRELIABLE;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.telephony.CellInfo;
import android.telephony.CellLocation;
import android.telephony.NeighboringCellInfo;
import android.telephony.PhoneStateListener;
import static android.telephony.PhoneStateListener.LISTEN_CELL_INFO;
import static android.telephony.PhoneStateListener.LISTEN_CELL_LOCATION;
import static android.telephony.PhoneStateListener.LISTEN_DATA_CONNECTION_STATE;
import static android.telephony.PhoneStateListener.LISTEN_NONE;
import static android.telephony.PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;
import static android.telephony.TelephonyManager.PHONE_TYPE_GSM;
import android.telephony.cdma.CdmaCellLocation;
import android.telephony.gsm.GsmCellLocation;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import org.mapsforge.core.graphics.Bitmap;
import org.mapsforge.core.graphics.Paint;
import org.mapsforge.core.graphics.Style;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.Dimension;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.Point;
import org.mapsforge.core.util.LatLongUtils;
import org.mapsforge.map.android.graphics.AndroidGraphicFactory;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.LayerManager;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.download.TileDownloadLayer;
import org.mapsforge.map.layer.download.tilesource.OnlineTileSource;
import org.mapsforge.map.layer.overlay.Circle;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.util.MapViewProjection;

import com.vonglasow.michael.satstat.R;
import com.vonglasow.michael.satstat.data.CellTower;
import com.vonglasow.michael.satstat.data.CellTowerCdma;
import com.vonglasow.michael.satstat.data.CellTowerGsm;
import com.vonglasow.michael.satstat.data.CellTowerList;
import com.vonglasow.michael.satstat.data.CellTowerListCdma;
import com.vonglasow.michael.satstat.data.CellTowerListGsm;
import com.vonglasow.michael.satstat.data.CellTowerListLte;
import com.vonglasow.michael.satstat.data.CellTowerLte;
import com.vonglasow.michael.satstat.mapsforge.PersistentTileCache;
import com.vonglasow.michael.satstat.widgets.GpsSnrView;
import com.vonglasow.michael.satstat.widgets.GpsStatusView;

public class MainActivity extends FragmentActivity implements ActionBar.TabListener, GpsStatus.Listener,
        LocationListener, OnSharedPreferenceChangeListener, SensorEventListener, ViewPager.OnPageChangeListener {

    public static double EARTH_CIRCUMFERENCE = 40000000; // meters

    /*
     * Indices into style arrays
     */
    private static final int STYLE_MARKER = 0;
    private static final int STYLE_STROKE = 1;
    private static final int STYLE_FILL = 2;

    /*
     * Styles for location providers
     */
    private static final String[] LOCATION_PROVIDER_STYLES = { "location_provider_blue", "location_provider_green",
            "location_provider_orange", "location_provider_purple", "location_provider_red" };

    /*
     * Blue style: default for network location provider
     */
    private static final String LOCATION_PROVIDER_BLUE = "location_provider_blue";

    /*
     * Red style: default for GPS location provider
     */
    private static final String LOCATION_PROVIDER_RED = "location_provider_red";

    /*
     * Gray style for inactive location providers
     */
    private static final String LOCATION_PROVIDER_GRAY = "location_provider_gray";

    private static final String KEY_LOCATION_STALE = "isStale";

    private static List<String> mAvailableProviderStyles;

    /**
     * The {@link android.support.v4.view.PagerAdapter} that will provide
     * fragments for each of the sections. We use a
     * {@link android.support.v4.app.FragmentPagerAdapter} derivative, which
     * will keep every loaded fragment in memory. If this becomes too memory
     * intensive, it may be best to switch to a
     * {@link android.support.v4.app.FragmentStatePagerAdapter}.
     */
    SectionsPagerAdapter mSectionsPagerAdapter;

    /**
     * The {@link ViewPager} that will host the section contents.
     */
    ViewPager mViewPager;

    /**
     * Whether the activity is stopped. 
     */
    boolean isStopped;

    /**
     * Whether we are running on a wide-screen device
     */
    boolean isWideScreen;

    //The rate in microseconds at which we would like to receive updates from the sensors.
    //private static final int iSensorRate = SensorManager.SENSOR_DELAY_UI;
    private static final int iSensorRate = 200000; //Default is 20,000 for accel, 5,000 for gyro

    private static LocationManager mLocationManager;
    private SensorManager mSensorManager;
    private Sensor mOrSensor;
    private Sensor mAccSensor;
    private Sensor mGyroSensor;
    private Sensor mMagSensor;
    private Sensor mLightSensor;
    private Sensor mProximitySensor;
    private Sensor mPressureSensor;
    private Sensor mHumiditySensor;
    private Sensor mTempSensor;

    /*
     *  Maximum resolutions for sensors, expressed as number of decimals. These
     *  values were chosen based on screen real estate and significance. They
     *  may be lowered if actual precision is lower, but will not be increased
     *  even if sensors are capable of delivering higher precision.
     */
    private byte mAccSensorRes = 3;
    private byte mGyroSensorRes = 4;
    private byte mMagSensorRes = 2;
    private byte mLightSensorRes = 1;
    private byte mProximitySensorRes = 1;
    private byte mPressureSensorRes = 0;
    private byte mHumiditySensorRes = 0;
    private byte mTempSensorRes = 1;

    private long mOrLast = 0;
    private long mAccLast = 0;
    private long mGyroLast = 0;
    private long mMagLast = 0;
    private long mLightLast = 0;
    private long mProximityLast = 0;
    private long mPressureLast = 0;
    private long mHumidityLast = 0;
    private long mTempLast = 0;

    private static CellTower mServingCell;
    private static CellTowerListGsm mCellsGsm = new CellTowerListGsm();
    private static CellTowerListCdma mCellsCdma = new CellTowerListCdma();
    private static CellTowerListLte mCellsLte = new CellTowerListLte();

    private static TelephonyManager mTelephonyManager;
    private static ConnectivityManager mConnectivityManager;
    private static WifiManager mWifiManager;

    protected static MenuItem menu_action_record;
    protected static MenuItem menu_action_stop_record;

    protected static boolean isGpsViewReady = false;
    protected static LinearLayout gpsRootLayout;
    protected static GpsStatusView gpsStatusView;
    protected static GpsSnrView gpsSnrView;
    protected static TextView gpsLat;
    protected static TextView gpsLon;
    protected static TextView orDeclination;
    protected static TextView gpsSpeed;
    protected static TextView gpsAlt;
    protected static TextView gpsTime;
    protected static TextView gpsBearing;
    protected static TextView gpsAccuracy;
    protected static TextView gpsOrientation;
    protected static TextView gpsSats;
    protected static TextView gpsTtff;

    protected static boolean isSensorViewReady = false;
    protected static TextView accStatus;
    protected static TextView accHeader;
    protected static TextView accTotal;
    protected static TextView accX;
    protected static TextView accY;
    protected static TextView accZ;
    protected static TextView rotStatus;
    protected static TextView rotHeader;
    protected static TextView rotTotal;
    protected static TextView rotX;
    protected static TextView rotY;
    protected static TextView rotZ;
    protected static TextView magStatus;
    protected static TextView magHeader;
    protected static TextView magTotal;
    protected static TextView magX;
    protected static TextView magY;
    protected static TextView magZ;
    protected static TextView orStatus;
    protected static TextView orHeader;
    protected static TextView orAzimuth;
    protected static TextView orAziText;
    protected static TextView orPitch;
    protected static TextView orRoll;
    protected static TextView miscHeader;
    protected static TextView tempStatus;
    protected static TextView tempHeader;
    protected static TextView metTemp;
    protected static TextView pressureStatus;
    protected static TextView pressureHeader;
    protected static TextView metPressure;
    protected static TextView humidStatus;
    protected static TextView humidHeader;
    protected static TextView metHumid;
    protected static TextView lightStatus;
    protected static TextView lightHeader;
    protected static TextView light;
    protected static TextView proximityStatus;
    protected static TextView proximityHeader;
    protected static TextView proximity;

    protected static boolean isRadioViewReady = false;
    protected static LinearLayout rilGsmLayout;
    protected static TableLayout rilCells;
    protected static LinearLayout rilCdmaLayout;
    protected static TableLayout rilCdmaCells;
    protected static LinearLayout rilLteLayout;
    protected static TableLayout rilLteCells;
    protected static LinearLayout wifiAps;

    protected static boolean isMapViewReady = false;
    protected static boolean isMapViewAttached = true;
    protected static MapView mapMap;
    protected static TileDownloadLayer mapDownloadLayer = null;
    protected static TileCache mapTileCache = null;
    protected static ImageButton mapReattach;
    protected static HashMap<String, Circle> mapCircles;
    protected static HashMap<String, Marker> mapMarkers;

    /**
     * Cached map of locations reported by the providers.
     * 
     * The keys correspond to the provider names as defined by LocationManager.
     * The entries are {@link Location} instances. For valid and recent
     * locations these are copies of the locations supplied by
     * {@link LocationManager}. Invalid locations, intended as placeholders,
     * have an empty provider string and should not be processed. Stale
     * locations have isStale entry in their extras set to true. They can be
     * processed but may require special handling.
     */
    protected static HashMap<String, Location> providerLocations;

    protected static HashMap<String, String> providerStyles;
    protected static HashMap<String, String> providerAppliedStyles;
    protected static Handler providerInvalidationHandler = null;
    protected static HashMap<String, Runnable> providerInvalidators;
    private static final int PROVIDER_EXPIRATION_DELAY = 2000; // the time after which a location is considered stale 

    private static List<ScanResult> scanResults = null;
    private static String selectedBSSID = "";
    protected static Handler networkTimehandler = null;
    protected static int mLastNetworkGen = 0; //the last observed (and displayed) network type
    protected static int mLastCellAsu = NeighboringCellInfo.UNKNOWN_RSSI;
    protected static int mLastCellDbm = CellTower.DBM_UNKNOWN;
    protected static Runnable networkTimeRunnable = null;
    private static final int NETWORK_REFRESH_DELAY = 1000; //the polling interval for the network type
    protected static Handler wifiTimehandler = null;
    protected static Runnable wifiTimeRunnable = null;
    private static final int WIFI_REFRESH_DELAY = 1000; //the time between two requests for WLAN rescan.

    /**
     * Converts screen rotation to orientation for devices with a naturally tall screen.
     */
    private final static Integer OR_FROM_ROT_TALL[] = { ActivityInfo.SCREEN_ORIENTATION_PORTRAIT,
            ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT,
            ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE };

    /**
     * Converts screen rotation to orientation for devices with a naturally wide screen.
     */
    private final static Integer OR_FROM_ROT_WIDE[] = { ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE,
            ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE,
            ActivityInfo.SCREEN_ORIENTATION_PORTRAIT };

    private static SharedPreferences mSharedPreferences;

    @SuppressLint("UseSparseArrays")
    private final static HashMap<Integer, Integer> channelsFrequency = new HashMap<Integer, Integer>() {
        /*
         * Required for serializable objects
         */
        private static final long serialVersionUID = 6793015643527778045L;

        {
            // 2.4 GHz (802.11 b/g/n)
            this.put(2412, 1);
            this.put(2417, 2);
            this.put(2422, 3);
            this.put(2427, 4);
            this.put(2432, 5);
            this.put(2437, 6);
            this.put(2442, 7);
            this.put(2447, 8);
            this.put(2452, 9);
            this.put(2457, 10);
            this.put(2462, 11);
            this.put(2467, 12);
            this.put(2472, 13);
            this.put(2484, 14);

            //5 GHz (802.11 a/h/j/n/ac)
            this.put(4915, 183);
            this.put(4920, 184);
            this.put(4925, 185);
            this.put(4935, 187);
            this.put(4940, 188);
            this.put(4945, 189);
            this.put(4960, 192);
            this.put(4980, 196);

            this.put(5035, 7);
            this.put(5040, 8);
            this.put(5045, 9);
            this.put(5055, 11);
            this.put(5060, 12);
            this.put(5080, 16);

            this.put(5170, 34);
            this.put(5180, 36);
            this.put(5190, 38);
            this.put(5200, 40);
            this.put(5210, 42);
            this.put(5220, 44);
            this.put(5230, 46);
            this.put(5240, 48);
            this.put(5260, 52);
            this.put(5280, 56);
            this.put(5300, 60);
            this.put(5320, 64);

            this.put(5500, 100);
            this.put(5520, 104);
            this.put(5540, 108);
            this.put(5560, 112);
            this.put(5580, 116);
            this.put(5600, 120);
            this.put(5620, 124);
            this.put(5640, 128);
            this.put(5660, 132);
            this.put(5680, 136);
            this.put(5700, 140);
            this.put(5745, 149);
            this.put(5765, 153);
            this.put(5785, 157);
            this.put(5805, 161);
            this.put(5825, 165);
        }
    };

    /** 
     * The {@link PhoneStateListener} for getting radio network updates 
     */
    private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
        // Requires API level 17. Many phones don't implement this method at 
        // all and will return null, the ones that do implement it return only
        // certain cell types.
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
        public void onCellInfoChanged(List<CellInfo> cellInfo) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)
                return;
            mCellsGsm.updateAll(cellInfo);
            mCellsCdma.updateAll(cellInfo);
            mCellsLte.updateAll(cellInfo);
            mServingCell = getServingCell(new CellTowerList[] { mCellsGsm, mCellsCdma, mCellsLte });
            showCells();
        }

        public void onCellLocationChanged(CellLocation location) {
            mCellsGsm.removeSource(CellTower.SOURCE_CELL_LOCATION);
            mCellsCdma.removeSource(CellTower.SOURCE_CELL_LOCATION);
            mCellsLte.removeSource(CellTower.SOURCE_CELL_LOCATION);
            String networkOperator = mTelephonyManager.getNetworkOperator();
            if (location instanceof GsmCellLocation) {
                if (mLastNetworkGen < 4) {
                    mServingCell = mCellsGsm.update(networkOperator, (GsmCellLocation) location);
                    if ((mServingCell.getDbm() == CellTower.DBM_UNKNOWN) && (mServingCell instanceof CellTowerGsm))
                        ((CellTowerGsm) mServingCell).setAsu(mLastCellAsu);
                } else {
                    mServingCell = mCellsLte.update(networkOperator, (GsmCellLocation) location);
                    if (mServingCell.getDbm() == CellTower.DBM_UNKNOWN)
                        ((CellTowerLte) mServingCell).setAsu(mLastCellAsu);
                }
            } else if (location instanceof CdmaCellLocation) {
                mServingCell = mCellsCdma.update((CdmaCellLocation) location);
                if (mServingCell.getDbm() == CellTower.DBM_UNKNOWN)
                    ((CellTowerCdma) mServingCell).setDbm(mLastCellDbm);
            }

            if (mTelephonyManager.getPhoneType() == PHONE_TYPE_GSM) {
                updateNeighboringCellInfo();
            }

            networkTimehandler.removeCallbacks(networkTimeRunnable);
            if ((mServingCell == null) || (mServingCell.getGeneration() <= 0)) {
                if ((mLastNetworkGen != 0) && (mServingCell != null))
                    mServingCell.setGeneration(mLastNetworkGen);
                NetworkInfo netinfo = mConnectivityManager.getActiveNetworkInfo();
                if ((netinfo == null) || (netinfo.getType() < ConnectivityManager.TYPE_MOBILE_MMS)
                        || (netinfo.getType() > ConnectivityManager.TYPE_MOBILE_HIPRI)) {
                    networkTimehandler.postDelayed(networkTimeRunnable, NETWORK_REFRESH_DELAY);
                }
            } else if (mServingCell != null) {
                mLastNetworkGen = mServingCell.getGeneration();
            }

            showCells();
        }

        public void onDataConnectionStateChanged(int state, int networkType) {
            onNetworkTypeChanged(networkType);
        }

        public void onSignalStrengthsChanged(SignalStrength signalStrength) {
            int pt = mTelephonyManager.getPhoneType();
            if (pt == PHONE_TYPE_GSM) {
                mLastCellAsu = signalStrength.getGsmSignalStrength();
                updateNeighboringCellInfo();
                if ((mServingCell != null) && (mServingCell instanceof CellTowerGsm))
                    ((CellTowerGsm) mServingCell).setAsu(mLastCellAsu);
            } else if (pt == PHONE_TYPE_CDMA) {
                mLastCellDbm = signalStrength.getCdmaDbm();
                if ((mServingCell != null) && (mServingCell instanceof CellTowerCdma))
                    mServingCell.setDbm(mLastCellDbm);
            }
            showCells();
        }
    };

    /** 
     * The {@link BroadcastReceiver} for getting radio network updates 
     */
    private final BroadcastReceiver mWifiScanReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context c, Intent intent) {
            if (intent.getAction() == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) {
                scanResults = mWifiManager.getScanResults();
                if (isRadioViewReady) {
                    refreshWifiResults();
                }
            } else {
                //something has changed about WiFi setup, rescan
                mWifiManager.startScan();
            }
        }
    };

    private Thread.UncaughtExceptionHandler defaultUEH;

    private final void onWifiEntryClick(String BSSID) {
        selectedBSSID = BSSID;
        refreshWifiResults();
    }

    private final void addWifiResult(ScanResult result) {
        final ScanResult r = result;
        android.view.View.OnClickListener clis = new android.view.View.OnClickListener() {

            @Override
            public void onClick(View v) {
                onWifiEntryClick(r.BSSID);
            }
        };

        View divider = new View(wifiAps.getContext());
        divider.setLayoutParams(new TableRow.LayoutParams(LayoutParams.MATCH_PARENT, 1));
        divider.setBackgroundColor(getResources().getColor(android.R.color.tertiary_text_dark));
        divider.setOnClickListener(clis);
        wifiAps.addView(divider);

        LinearLayout wifiLayout = new LinearLayout(wifiAps.getContext());
        wifiLayout.setLayoutParams(
                new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        wifiLayout.setOrientation(LinearLayout.HORIZONTAL);
        wifiLayout.setWeightSum(22);
        wifiLayout.setMeasureWithLargestChildEnabled(false);

        ImageView wifiType = new ImageView(wifiAps.getContext());
        wifiType.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.MATCH_PARENT, 3));
        if (WifiCapabilities.isAdhoc(result)) {
            wifiType.setImageResource(R.drawable.ic_content_wifi_adhoc);
        } else if ((WifiCapabilities.isEnterprise(result))
                || (WifiCapabilities.getScanResultSecurity(result) == WifiCapabilities.EAP)) {
            wifiType.setImageResource(R.drawable.ic_content_wifi_eap);
        } else if (WifiCapabilities.getScanResultSecurity(result) == WifiCapabilities.PSK) {
            wifiType.setImageResource(R.drawable.ic_content_wifi_psk);
        } else if (WifiCapabilities.getScanResultSecurity(result) == WifiCapabilities.WEP) {
            wifiType.setImageResource(R.drawable.ic_content_wifi_wep);
        } else if (WifiCapabilities.getScanResultSecurity(result) == WifiCapabilities.OPEN) {
            wifiType.setImageResource(R.drawable.ic_content_wifi_open);
        } else {
            wifiType.setImageResource(R.drawable.ic_content_wifi_unknown);
        }

        wifiType.setScaleType(ScaleType.CENTER);
        wifiLayout.addView(wifiType);

        TableLayout wifiDetails = new TableLayout(wifiAps.getContext());
        wifiDetails.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 19));
        TableRow innerRow1 = new TableRow(wifiAps.getContext());
        TextView newMac = new TextView(wifiAps.getContext());
        newMac.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 14));
        newMac.setTextAppearance(wifiAps.getContext(), android.R.style.TextAppearance_Medium);
        newMac.setText(result.BSSID);
        innerRow1.addView(newMac);
        TextView newCh = new TextView(wifiAps.getContext());
        newCh.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 2));
        newCh.setTextAppearance(wifiAps.getContext(), android.R.style.TextAppearance_Medium);
        newCh.setText(getChannelFromFrequency(result.frequency));
        innerRow1.addView(newCh);
        TextView newLevel = new TextView(wifiAps.getContext());
        newLevel.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newLevel.setTextAppearance(wifiAps.getContext(), android.R.style.TextAppearance_Medium);
        newLevel.setText(String.valueOf(result.level));
        innerRow1.addView(newLevel);
        innerRow1.setOnClickListener(clis);
        wifiDetails.addView(innerRow1,
                new TableLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

        TableRow innerRow2 = new TableRow(wifiAps.getContext());
        TextView newSSID = new TextView(wifiAps.getContext());
        newSSID.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 19));
        newSSID.setTextAppearance(wifiAps.getContext(), android.R.style.TextAppearance_Small);
        newSSID.setText(result.SSID);
        innerRow2.addView(newSSID);
        innerRow2.setOnClickListener(clis);
        wifiDetails.addView(innerRow2,
                new TableLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

        wifiLayout.addView(wifiDetails);
        wifiLayout.setOnClickListener(clis);
        wifiAps.addView(wifiLayout);
    }

    private final void refreshWifiResults() {
        if (scanResults != null) {
            wifiAps.removeAllViews();
            //add the selected network first
            for (ScanResult result : scanResults) {
                if (result.BSSID.equals(selectedBSSID)) {
                    addWifiResult(result);
                }
            }
            for (ScanResult result : scanResults) {
                if (!result.BSSID.equals(selectedBSSID)) {
                    addWifiResult(result);
                }
            }
        }
    }

    /**
     * Converts an accuracy value into a color identifier.
     */
    public static int accuracyToColor(int accuracy) {
        switch (accuracy) {
        case SENSOR_STATUS_ACCURACY_HIGH:
            return (R.color.accHigh);
        case SENSOR_STATUS_ACCURACY_MEDIUM:
            return (R.color.accMedium);
        case SENSOR_STATUS_ACCURACY_LOW:
            return (R.color.accLow);
        case SENSOR_STATUS_UNRELIABLE:
            return (R.color.accUnreliable);
        default:
            return (android.R.color.background_dark);
        }
    }

    /**
     * Applies a style to the map overlays associated with a given location provider.
     * 
     * This method changes the style (effectively, the color) of the circle and
     * marker overlays. Its main purpose is to switch the color of the overlays
     * between gray and the provider color.
     * 
     * @param context The context of the caller
     * @param provider The name of the location provider, as returned by
     * {@link LocationProvider.getName()}.
     * @param styleName The name of the style to apply. If it is null, the
     * default style for the provider as returned by 
     * assignLocationProviderStyle() is applied. 
     */
    protected static void applyLocationProviderStyle(Context context, String provider, String styleName) {
        String sn = (styleName != null) ? styleName : assignLocationProviderStyle(provider);

        Boolean isStyleChanged = !sn.equals(providerAppliedStyles.get(provider));
        Boolean needsRedraw = false;

        Resources res = context.getResources();
        TypedArray style = res.obtainTypedArray(res.getIdentifier(sn, "array", context.getPackageName()));

        // Circle layer
        Circle circle = mapCircles.get(provider);
        if (circle != null) {
            circle.getPaintFill().setColor(style.getColor(STYLE_FILL, R.color.circle_gray_fill));
            circle.getPaintStroke().setColor(style.getColor(STYLE_STROKE, R.color.circle_gray_stroke));
            needsRedraw = isStyleChanged && circle.isVisible();
        }

        //Marker layer
        Marker marker = mapMarkers.get(provider);
        if (marker != null) {
            Drawable drawable = style.getDrawable(STYLE_MARKER);
            Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable);
            marker.setBitmap(bitmap);
            needsRedraw = needsRedraw || (isStyleChanged && marker.isVisible());
        }

        if (needsRedraw)
            mapMap.getLayerManager().redrawLayers();
        providerAppliedStyles.put(provider, sn);
        style.recycle();
    }

    /**
     * Returns the map overlay style to use for a given location provider.
     * 
     * This method first checks if a style has already been assigned to the
     * location provider. In that case the already assigned style is returned.
     * Otherwise a new style is assigned and the assignment is stored
     * internally and written to SharedPreferences.
     * @param provider
     * @return The style to use for non-stale locations
     */
    protected static String assignLocationProviderStyle(String provider) {
        String styleName = providerStyles.get(provider);
        if (styleName == null) {
            /*
             * Not sure if this ever happens but I can't rule it out. Scenarios I can think of:
             * - A custom location provider which identifies itself as "passive"
             * - A combination of the following:
             *   - Passive location provider is selected
             *   - A new provider is added while we're running (so it's not in our list)
             *   - Another app starts using the new provider
             *   - The passive location provider forwards us an update from the new provider
             */
            if (mAvailableProviderStyles.isEmpty())
                mAvailableProviderStyles.addAll(Arrays.asList(LOCATION_PROVIDER_STYLES));
            styleName = mSharedPreferences.getString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + provider,
                    mAvailableProviderStyles.get(0));
            providerStyles.put(provider, styleName);
            SharedPreferences.Editor spEditor = mSharedPreferences.edit();
            spEditor.putString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + provider, styleName);
            spEditor.commit();
        }
        return styleName;
    }

    /**
     * Formats an item of cell information data for display.
     * <p>
     * This helper function formats any item of cell information data, such as
     * the cell ID, PSC or similar. For valid data a string with the properly
     * formatted value will be returned. If the input value is
     * {@link com.vonglasow.michael.satstat.data.CellTower#UNKNOWN}, then the
     * {@code value_none} resource string will be returned. 
     * @param context the context of the caller
     * @param format a format string, which must contain placeholders for exactly one variable, or {@code null}.
     * @param raw the value to format
     * @return
     */
    public static String formatCellData(Context context, String format, int raw) {
        if (raw == CellTower.UNKNOWN)
            return context.getResources().getString(R.string.value_none);
        else {
            String fmt = (format != null) ? format : "%d";
            return String.format(fmt, raw);
        }
    }

    /**
     * Formats cell signal strength for display.
     * <p>
     * This helper function formats the signal strength for a cell. For valid
     * data a string with the properly formatted value will be returned. If the
     * input value is
     * {@link com.vonglasow.michael.satstat.data.CellTower#DBM_UNKNOWN}, then
     * the {@code value_none} resource string will be returned.
     * @param context the context of the caller
     * @param format a format string, which must contain placeholders for exactly one variable, or {@code null}.
     * @param raw the signal strength in dBm
     * @return
     */
    public static String formatCellDbm(Context context, String format, int raw) {
        if (raw == CellTower.DBM_UNKNOWN)
            return context.getResources().getString(R.string.value_none);
        else {
            String fmt = (format != null) ? format : "%d";
            return String.format(fmt, raw);
        }
    }

    /**
     * Converts a bearing (in degrees) into a directional name.
     */
    public String formatOrientation(float bearing) {
        return (bearing < 11.25) ? getString(R.string.value_N)
                : (bearing < 33.75) ? getString(R.string.value_NNE)
                        : (bearing < 56.25) ? getString(R.string.value_NE)
                                : (bearing < 78.75) ? getString(R.string.value_ENE)
                                        : (bearing < 101.25) ? getString(R.string.value_E)
                                                : (bearing < 123.75) ? getString(R.string.value_ESE)
                                                        : (bearing < 146.25) ? getString(R.string.value_SE)
                                                                : (bearing < 168.75) ? getString(R.string.value_SSE)
                                                                        : (bearing < 191.25)
                                                                                ? getString(R.string.value_S)
                                                                                : (bearing < 213.75)
                                                                                        ? getString(
                                                                                                R.string.value_SSW)
                                                                                        : (bearing < 236.25)
                                                                                                ? getString(
                                                                                                        R.string.value_SW)
                                                                                                : (bearing < 258.75)
                                                                                                        ? getString(
                                                                                                                R.string.value_WSW)
                                                                                                        : (bearing < 280.25)
                                                                                                                ? getString(
                                                                                                                        R.string.value_W)
                                                                                                                : (bearing < 302.75)
                                                                                                                        ? getString(
                                                                                                                                R.string.value_WNW)
                                                                                                                        : (bearing < 325.25)
                                                                                                                                ? getString(
                                                                                                                                        R.string.value_NW)
                                                                                                                                : (bearing < 347.75)
                                                                                                                                        ? getString(
                                                                                                                                                R.string.value_NNW)
                                                                                                                                        : getString(
                                                                                                                                                R.string.value_N);
    }

    /**
     * Gets the WiFi channel number for a frequency
     * @param frequency The frequency in MHz
     * @return The channel number corresponding to {@code frequency}
     */
    public static String getChannelFromFrequency(int frequency) {
        if (channelsFrequency.containsKey(frequency)) {
            return String.valueOf(channelsFrequency.get(frequency));
        } else {
            return "?";
        }
    }

    /**
     * Gets the display color for a phone network generation.
     * @param generation The network generation, i.e. {@code 2}, {@code 3} or {@code 4} for any flavor of 2G, 3G or 4G, or {@code 0} for unknown
     * @return The color in which to display the indicator. If {@code generation} is {@code 0} or not a valid generation, the color returned will be transparent.
     */
    public static int getColorFromGeneration(int generation) {
        switch (generation) {
        case 2:
            return (R.color.gen2);
        case 3:
            return (R.color.gen3);
        case 4:
            return (R.color.gen4);
        default:
            return (android.R.color.transparent);
        }
    }

    /**
     * Gets the generation of a phone network type
     * @param networkType The network type as returned by {@link TelephonyManager.getNetworkType}
     * @return 2, 3 or 4 for 2G, 3G or 4G; 0 for unknown
     */
    public static int getNetworkGeneration(int networkType) {
        switch (networkType) {
        case TelephonyManager.NETWORK_TYPE_CDMA:
        case TelephonyManager.NETWORK_TYPE_EDGE:
        case TelephonyManager.NETWORK_TYPE_GPRS:
        case TelephonyManager.NETWORK_TYPE_IDEN:
            return 2;
        case TelephonyManager.NETWORK_TYPE_1xRTT:
        case TelephonyManager.NETWORK_TYPE_EHRPD:
        case TelephonyManager.NETWORK_TYPE_EVDO_0:
        case TelephonyManager.NETWORK_TYPE_EVDO_A:
        case TelephonyManager.NETWORK_TYPE_EVDO_B:
        case TelephonyManager.NETWORK_TYPE_HSDPA:
        case TelephonyManager.NETWORK_TYPE_HSPA:
        case TelephonyManager.NETWORK_TYPE_HSPAP:
        case TelephonyManager.NETWORK_TYPE_HSUPA:
        case TelephonyManager.NETWORK_TYPE_UMTS:
            return 3;
        case TelephonyManager.NETWORK_TYPE_LTE:
            return 4;
        default:
            return 0;
        }
    }

    /**
     * Gets the number of decimal digits to show when displaying sensor values, based on sensor accuracy.
     * @param sensor The sensor
     * @param maxDecimals The maximum number of decimals to display, even if the sensor's accuracy is higher
     * @return
     */
    public static byte getSensorDecimals(Sensor sensor, byte maxDecimals) {
        if (sensor == null)
            return 0;
        float res = sensor.getResolution();
        if (res == 0)
            return maxDecimals;
        return (byte) Math.min(maxDecimals,
                (sensor != null) ? (byte) Math.max(Math.ceil((float) -Math.log10(sensor.getResolution())), 0) : 0);
    }

    /**
     * Returns the serving cell.
     * <p>
     * This method iterates through the cell tower lists passed in
     * {@code lists} and looks for any entries marked as the serving cell.
     *  
     * @param lists An array of {@link com.vonglasow.michael.satstat.data.CellTowerList}
     * instances
     * @return The serving cell, if one is found, or {@code null} if none is
     * found. If multiple serving cells are found in {@code lists}, no
     * assertion is made which cell will be returned, or even that results
     * will be consistent between calls.
     */
    public static CellTower getServingCell(CellTowerList[] lists) {
        for (CellTowerList<CellTower> towers : lists) {
            for (CellTower cell : towers.getAll())
                if (cell.hasSource() && cell.isServing())
                    return cell;
        }
        return null;
    }

    /**
     * Determines if a location is stale.
     * 
     * A location is considered stale if its Extras have an isStale key set to
     * True. A location without this key is not considered stale.
     * 
     * @param location
     * @return True if stale, False otherwise
     */
    public static boolean isLocationStale(Location location) {
        Bundle extras = location.getExtras();
        if (extras == null)
            return false;
        return extras.getBoolean(KEY_LOCATION_STALE);
    }

    public static void markLocationAsStale(Location location) {
        if (location.getExtras() == null)
            location.setExtras(new Bundle());
        location.getExtras().putBoolean(KEY_LOCATION_STALE, true);
    }

    /**
     * Called when a sensor's accuracy has changed. Does nothing.
     */
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        defaultUEH = Thread.getDefaultUncaughtExceptionHandler();

        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
            public void uncaughtException(Thread t, Throwable e) {
                Context c = getApplicationContext();
                File dumpDir = c.getExternalFilesDir(null);
                File dumpFile = new File(dumpDir, "satstat-" + System.currentTimeMillis() + ".log");
                PrintStream s;
                try {
                    InputStream buildInStream = getResources().openRawResource(R.raw.build);
                    s = new PrintStream(dumpFile);
                    s.append("SatStat build: ");

                    int i;
                    try {
                        i = buildInStream.read();
                        while (i != -1) {
                            s.write(i);
                            i = buildInStream.read();
                        }
                        buildInStream.close();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }

                    s.append("\n\n");
                    e.printStackTrace(s);
                    s.flush();
                    s.close();
                } catch (FileNotFoundException e2) {
                    e2.printStackTrace();
                }
                defaultUEH.uncaughtException(t, e);
            }
        });

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mSharedPreferences.registerOnSharedPreferenceChangeListener(this);

        final ActionBar actionBar = getActionBar();

        setContentView(R.layout.activity_main);

        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

        // Find out default screen orientation
        Configuration config = getResources().getConfiguration();
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        int rot = wm.getDefaultDisplay().getRotation();
        isWideScreen = (config.orientation == Configuration.ORIENTATION_LANDSCAPE
                && (rot == Surface.ROTATION_0 || rot == Surface.ROTATION_180)
                || config.orientation == Configuration.ORIENTATION_PORTRAIT
                        && (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270));
        Log.d("MainActivity", "isWideScreen=" + Boolean.toString(isWideScreen));

        // compact action bar
        int dpX = (int) (this.getResources().getDisplayMetrics().widthPixels
                / this.getResources().getDisplayMetrics().density);
        /*
         * This is a crude way to ensure a one-line action bar with tabs
         * (not a drop-down list) and home (incon) and title only if there
         * is space, depending on screen width:
         * divide screen in units of 64 dp
         * each tab requires 1 unit, home and menu require slightly less,
         * title takes up approx. 2.5 units in portrait,
         * home and title are about 2 units wide in landscape
         */
        if (dpX < 192) {
            // just enough space for drop-down list and menu
            actionBar.setDisplayShowHomeEnabled(false);
            actionBar.setDisplayShowTitleEnabled(false);
        } else if (dpX < 320) {
            // not enough space for four tabs, but home will fit next to list
            actionBar.setDisplayShowHomeEnabled(true);
            actionBar.setDisplayShowTitleEnabled(false);
        } else if (dpX < 384) {
            // just enough space for four tabs
            actionBar.setDisplayShowHomeEnabled(false);
            actionBar.setDisplayShowTitleEnabled(false);
        } else if ((dpX < 448) || ((config.orientation == Configuration.ORIENTATION_PORTRAIT) && (dpX < 544))) {
            // space for four tabs and home, but not title
            actionBar.setDisplayShowHomeEnabled(true);
            actionBar.setDisplayShowTitleEnabled(false);
        } else {
            // ample space for home, title and all four tabs
            actionBar.setDisplayShowHomeEnabled(true);
            actionBar.setDisplayShowTitleEnabled(true);
        }
        setEmbeddedTabs(actionBar, true);

        providerLocations = new HashMap<String, Location>();

        mAvailableProviderStyles = new ArrayList<String>(Arrays.asList(LOCATION_PROVIDER_STYLES));

        providerStyles = new HashMap<String, String>();
        providerAppliedStyles = new HashMap<String, String>();

        providerInvalidationHandler = new Handler();
        providerInvalidators = new HashMap<String, Runnable>();

        // Create the adapter that will return a fragment for each of the three
        // primary sections of the app.
        mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

        // Set up the ViewPager with the sections adapter.
        mViewPager = (ViewPager) findViewById(R.id.pager);
        mViewPager.setAdapter(mSectionsPagerAdapter);
        mViewPager.setOnPageChangeListener(this);

        // Add tabs, specifying the tab's text and TabListener
        for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) {
            actionBar.addTab(actionBar.newTab()
                    //.setText(mSectionsPagerAdapter.getPageTitle(i))
                    .setIcon(mSectionsPagerAdapter.getPageIcon(i)).setTabListener(this));
        }

        // This is needed by the mapsforge library.
        AndroidGraphicFactory.createInstance(this.getApplication());

        // Get system services for event delivery
        mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        mOrSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
        mAccSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mGyroSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
        mMagSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        mProximitySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
        mPressureSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
        mHumiditySensor = mSensorManager.getDefaultSensor(Sensor.TYPE_RELATIVE_HUMIDITY);
        mTempSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_AMBIENT_TEMPERATURE);
        mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);

        mAccSensorRes = getSensorDecimals(mAccSensor, mAccSensorRes);
        mGyroSensorRes = getSensorDecimals(mGyroSensor, mGyroSensorRes);
        mMagSensorRes = getSensorDecimals(mMagSensor, mMagSensorRes);
        mLightSensorRes = getSensorDecimals(mLightSensor, mLightSensorRes);
        mProximitySensorRes = getSensorDecimals(mProximitySensor, mProximitySensorRes);
        mPressureSensorRes = getSensorDecimals(mPressureSensor, mPressureSensorRes);
        mHumiditySensorRes = getSensorDecimals(mHumiditySensor, mHumiditySensorRes);
        mTempSensorRes = getSensorDecimals(mTempSensor, mTempSensorRes);

        networkTimehandler = new Handler();
        networkTimeRunnable = new Runnable() {
            @Override
            public void run() {
                int newNetworkType = mTelephonyManager.getNetworkType();
                if (getNetworkGeneration(newNetworkType) != mLastNetworkGen)
                    onNetworkTypeChanged(newNetworkType);
                else
                    networkTimehandler.postDelayed(this, NETWORK_REFRESH_DELAY);
            }
        };

        wifiTimehandler = new Handler();
        wifiTimeRunnable = new Runnable() {

            @Override
            public void run() {
                mWifiManager.startScan();
                wifiTimehandler.postDelayed(this, WIFI_REFRESH_DELAY);
            }
        };

        updateLocationProviderStyles();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);

        return true;
    }

    @Override
    protected void onDestroy() {
        mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
        super.onDestroy();
    }

    /**
     * Called when the status of the GPS changes. Updates GPS display.
     */
    public void onGpsStatusChanged(int event) {
        GpsStatus status = mLocationManager.getGpsStatus(null);
        int satsInView = 0;
        int satsUsed = 0;
        Iterable<GpsSatellite> sats = status.getSatellites();
        for (GpsSatellite sat : sats) {
            satsInView++;
            if (sat.usedInFix()) {
                satsUsed++;
            }
        }

        if (isGpsViewReady) {
            gpsSats.setText(String.valueOf(satsUsed) + "/" + String.valueOf(satsInView));
            gpsTtff.setText(String.valueOf(status.getTimeToFirstFix() / 1000));
            gpsStatusView.showSats(sats);
            gpsSnrView.showSats(sats);
        }

        if ((isMapViewReady) && (satsUsed == 0)) {
            Location location = providerLocations.get(LocationManager.GPS_PROVIDER);
            if (location != null)
                markLocationAsStale(location);
            applyLocationProviderStyle(this, LocationManager.GPS_PROVIDER, LOCATION_PROVIDER_GRAY);
        }
    }

    /**
     * Called when a new location is found by a registered location provider.
     * Stores the location and updates GPS display and map view.
     */
    public void onLocationChanged(Location location) {
        // some providers may report NaN for latitude and longitude:
        // if that happens, do not process this location and mark any previous
        // location from that provider as stale
        if (Double.isNaN(location.getLatitude()) || Double.isNaN(location.getLongitude())) {
            markLocationAsStale(providerLocations.get(location.getProvider()));
            if (isMapViewReady)
                applyLocationProviderStyle(this, location.getProvider(), LOCATION_PROVIDER_GRAY);
            return;
        }

        if (providerLocations.containsKey(location.getProvider()))
            providerLocations.put(location.getProvider(), new Location(location));

        // update map view
        if (isMapViewReady) {
            LatLong latLong = new LatLong(location.getLatitude(), location.getLongitude());

            Circle circle = mapCircles.get(location.getProvider());
            Marker marker = mapMarkers.get(location.getProvider());

            if (circle != null) {
                circle.setLatLong(latLong);
                if (location.hasAccuracy()) {
                    circle.setVisible(true);
                    circle.setRadius(location.getAccuracy());
                } else {
                    Log.d("MainActivity", "Location from " + location.getProvider() + " has no accuracy");
                    circle.setVisible(false);
                }
            }

            if (marker != null) {
                marker.setLatLong(latLong);
                marker.setVisible(true);
            }

            applyLocationProviderStyle(this, location.getProvider(), null);

            Runnable invalidator = providerInvalidators.get(location.getProvider());
            if (invalidator != null) {
                providerInvalidationHandler.removeCallbacks(invalidator);
                providerInvalidationHandler.postDelayed(invalidator, PROVIDER_EXPIRATION_DELAY);
            }

            // redraw, move locations into view and zoom out as needed
            if ((circle != null) || (marker != null) || (invalidator != null))
                updateMap();
        }

        // update GPS view
        if ((location.getProvider().equals(LocationManager.GPS_PROVIDER)) && (isGpsViewReady)) {
            if (location.hasAccuracy()) {
                gpsAccuracy.setText(String.format("%.0f", location.getAccuracy()));
            } else {
                gpsAccuracy.setText(getString(R.string.value_none));
            }

            gpsLat.setText(String.format("%.5f%s", location.getLatitude(), getString(R.string.unit_degree)));
            gpsLon.setText(String.format("%.5f%s", location.getLongitude(), getString(R.string.unit_degree)));
            gpsTime.setText(String.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS", location.getTime()));

            if (location.hasAltitude()) {
                gpsAlt.setText(String.format("%.0f", location.getAltitude()));
                orDeclination.setText(String.format("%.0f%s",
                        new GeomagneticField((float) location.getLatitude(), (float) location.getLongitude(),
                                (float) location.getAltitude(), location.getTime()).getDeclination(),
                        getString(R.string.unit_degree)));
            } else {
                gpsAlt.setText(getString(R.string.value_none));
                orDeclination.setText(getString(R.string.value_none));
            }

            if (location.hasBearing()) {
                gpsBearing.setText(String.format("%.0f%s", location.getBearing(), getString(R.string.unit_degree)));
                gpsOrientation.setText(formatOrientation(location.getBearing()));
            } else {
                gpsBearing.setText(getString(R.string.value_none));
                gpsOrientation.setText(getString(R.string.value_none));
            }

            if (location.hasSpeed()) {
                gpsSpeed.setText(String.format("%.0f", (location.getSpeed()) * 3.6));
            } else {
                gpsSpeed.setText(getString(R.string.value_none));
            }

            // note: getting number of sats in fix by looking for "satellites"
            // in location's extras doesn't seem to work, always returns 0 sats
        }
    }

    /**
     * Called when a menu item is selected, and triggers the appropriate action.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_agps:
            Log.i(this.getLocalClassName(), "User requested AGPS data update");
            GpsEventReceiver.refreshAgps(this, false, true);
            return true;
        case R.id.action_settings:
            startActivity(new Intent(this, SettingsActivity.class));
            return true;
        case R.id.action_about:
            startActivity(new Intent(this, AboutActivity.class));
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    /**
     * Updates the network type indicator for the current cell. Called by
     * {@link networkTimeRunnable.run()} or
     * {@link android.telephony.PhoneStateListener.onDataConnectionChanged(int, int)}.
     * 
     * @param networkType One of the NETWORK_TYPE_xxxx constants defined in {@link android.telephony.TelephonyManager}
     */
    protected static void onNetworkTypeChanged(int networkType) {
        Log.d("MainActivity", "Network type changed to " + Integer.toString(networkType));
        int newNetworkGen = getNetworkGeneration(networkType);
        if (newNetworkGen != mLastNetworkGen) {
            networkTimehandler.removeCallbacks(networkTimeRunnable);
            // if we switched from GSM/UMTS to LTE or vice versa, the cell may
            // have been stored in the wrong list
            if ((newNetworkGen == 4) || (mLastNetworkGen == 4)) {
                CellLocation cellLocation = mTelephonyManager.getCellLocation();
                String networkOperator = mTelephonyManager.getNetworkOperator();
                if (newNetworkGen == 4) {
                    mCellsGsm.removeSource(CellTower.SOURCE_CELL_LOCATION | CellTower.SOURCE_NEIGHBORING_CELL_INFO
                            | CellTower.SOURCE_CELL_INFO);
                    if (cellLocation instanceof GsmCellLocation)
                        mServingCell = mCellsLte.update(networkOperator, (GsmCellLocation) cellLocation);
                } else {
                    mCellsLte.removeSource(CellTower.SOURCE_CELL_LOCATION | CellTower.SOURCE_NEIGHBORING_CELL_INFO
                            | CellTower.SOURCE_CELL_INFO);
                    if (cellLocation instanceof GsmCellLocation)
                        mServingCell = mCellsGsm.update(networkOperator, (GsmCellLocation) cellLocation);
                }
            }

            mLastNetworkGen = newNetworkGen;
            if (mServingCell != null) {
                mServingCell.setNetworkType(networkType);
                Log.d(MainActivity.class.getSimpleName(),
                        String.format("Setting network type to %d for cell %s (%s)", mServingCell.getGeneration(),
                                mServingCell.getText(), mServingCell.getAltText()));
            }
        }
        showCells();
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        // TODO Auto-generated method stub

    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        // TODO Auto-generated method stub

    }

    @Override
    public void onPageSelected(int position) {
        // When swiping between pages, select the
        // corresponding tab.
        getActionBar().setSelectedNavigationItem(position);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if ((isMapViewReady) && (mapDownloadLayer != null))
            mapDownloadLayer.onPause();
    }

    /**
     * Called when a location provider is disabled. Does nothing.
     */
    public void onProviderDisabled(String provider) {
    }

    /**
     * Called when a location provider is enabled. Does nothing.
     */
    public void onProviderEnabled(String provider) {
    }

    @Override
    protected void onResume() {
        super.onResume();
        isStopped = false;
        registerLocationProviders(this);
        mLocationManager.addGpsStatusListener(this);
        mSensorManager.registerListener(this, mOrSensor, iSensorRate);
        mSensorManager.registerListener(this, mAccSensor, iSensorRate);
        mSensorManager.registerListener(this, mGyroSensor, iSensorRate);
        mSensorManager.registerListener(this, mMagSensor, iSensorRate);
        mSensorManager.registerListener(this, mLightSensor, iSensorRate);
        mSensorManager.registerListener(this, mProximitySensor, iSensorRate);
        mSensorManager.registerListener(this, mPressureSensor, iSensorRate);
        mSensorManager.registerListener(this, mHumiditySensor, iSensorRate);
        mSensorManager.registerListener(this, mTempSensor, iSensorRate);
        mTelephonyManager.listen(mPhoneStateListener,
                (LISTEN_CELL_INFO | LISTEN_CELL_LOCATION | LISTEN_DATA_CONNECTION_STATE | LISTEN_SIGNAL_STRENGTHS));

        // register for certain WiFi events indicating that new networks may be in range
        // An access point scan has completed, and results are available.
        registerReceiver(mWifiScanReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));

        // The state of Wi-Fi connectivity has changed.
        registerReceiver(mWifiScanReceiver, new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));

        // The RSSI (signal strength) has changed.
        registerReceiver(mWifiScanReceiver, new IntentFilter(WifiManager.RSSI_CHANGED_ACTION));

        // A connection to the supplicant has been established or the connection to the supplicant has been lost.
        registerReceiver(mWifiScanReceiver, new IntentFilter(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION));

        wifiTimehandler.postDelayed(wifiTimeRunnable, WIFI_REFRESH_DELAY);

        if ((isMapViewReady) && (mapDownloadLayer != null))
            mapDownloadLayer.onResume();
    }

    /**
     * Called when a sensor's reading changes. Updates sensor display.
     */
    public void onSensorChanged(SensorEvent event) {
        //to enforce sensor rate
        boolean isRateElapsed = false;

        switch (event.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER:
            isRateElapsed = (event.timestamp / 1000) - mAccLast >= iSensorRate;
            // if Z acceleration is greater than X/Y combined, lock rotation, else unlock
            if (Math.pow(event.values[2], 2) > Math.pow(event.values[0], 2) + Math.pow(event.values[1], 2)) {
                // workaround (SCREEN_ORIENTATION_LOCK is unsupported on API < 18)
                if (isWideScreen)
                    setRequestedOrientation(
                            OR_FROM_ROT_WIDE[this.getWindowManager().getDefaultDisplay().getRotation()]);
                else
                    setRequestedOrientation(
                            OR_FROM_ROT_TALL[this.getWindowManager().getDefaultDisplay().getRotation()]);
            } else {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
            }
            break;
        case Sensor.TYPE_ORIENTATION:
            isRateElapsed = (event.timestamp / 1000) - mOrLast >= iSensorRate;
            break;
        case Sensor.TYPE_GYROSCOPE:
            isRateElapsed = (event.timestamp / 1000) - mGyroLast >= iSensorRate;
            break;
        case Sensor.TYPE_MAGNETIC_FIELD:
            isRateElapsed = (event.timestamp / 1000) - mMagLast >= iSensorRate;
            break;
        case Sensor.TYPE_LIGHT:
            isRateElapsed = (event.timestamp / 1000) - mLightLast >= iSensorRate;
            break;
        case Sensor.TYPE_PROXIMITY:
            isRateElapsed = (event.timestamp / 1000) - mProximityLast >= iSensorRate;
            break;
        case Sensor.TYPE_PRESSURE:
            isRateElapsed = (event.timestamp / 1000) - mPressureLast >= iSensorRate;
            break;
        case Sensor.TYPE_RELATIVE_HUMIDITY:
            isRateElapsed = (event.timestamp / 1000) - mHumidityLast >= iSensorRate;
            break;
        case Sensor.TYPE_AMBIENT_TEMPERATURE:
            isRateElapsed = (event.timestamp / 1000) - mTempLast >= iSensorRate;
            break;
        }

        if (isSensorViewReady && isRateElapsed) {
            switch (event.sensor.getType()) {
            case Sensor.TYPE_ACCELEROMETER:
                mAccLast = event.timestamp / 1000;
                accX.setText(String.format("%." + mAccSensorRes + "f", event.values[0]));
                accY.setText(String.format("%." + mAccSensorRes + "f", event.values[1]));
                accZ.setText(String.format("%." + mAccSensorRes + "f", event.values[2]));
                accTotal.setText(String.format("%." + mAccSensorRes + "f", Math.sqrt(Math.pow(event.values[0], 2)
                        + Math.pow(event.values[1], 2) + Math.pow(event.values[2], 2))));
                accStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_ORIENTATION:
                mOrLast = event.timestamp / 1000;
                orAzimuth.setText(String.format("%.0f%s", event.values[0], getString(R.string.unit_degree)));
                orAziText.setText(formatOrientation(event.values[0]));
                orPitch.setText(String.format("%.0f%s", event.values[1], getString(R.string.unit_degree)));
                orRoll.setText(String.format("%.0f%s", event.values[2], getString(R.string.unit_degree)));
                orStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_GYROSCOPE:
                mGyroLast = event.timestamp / 1000;
                rotX.setText(String.format("%." + mGyroSensorRes + "f", event.values[0]));
                rotY.setText(String.format("%." + mGyroSensorRes + "f", event.values[1]));
                rotZ.setText(String.format("%." + mGyroSensorRes + "f", event.values[2]));
                rotTotal.setText(String.format("%." + mGyroSensorRes + "f", Math.sqrt(Math.pow(event.values[0], 2)
                        + Math.pow(event.values[1], 2) + Math.pow(event.values[2], 2))));
                rotStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_MAGNETIC_FIELD:
                mMagLast = event.timestamp / 1000;
                magX.setText(String.format("%." + mMagSensorRes + "f", event.values[0]));
                magY.setText(String.format("%." + mMagSensorRes + "f", event.values[1]));
                magZ.setText(String.format("%." + mMagSensorRes + "f", event.values[2]));
                magTotal.setText(String.format("%." + mMagSensorRes + "f", Math.sqrt(Math.pow(event.values[0], 2)
                        + Math.pow(event.values[1], 2) + Math.pow(event.values[2], 2))));
                magStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_LIGHT:
                mLightLast = event.timestamp / 1000;
                light.setText(String.format("%." + mLightSensorRes + "f", event.values[0]));
                lightStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_PROXIMITY:
                mProximityLast = event.timestamp / 1000;
                proximity.setText(String.format("%." + mProximitySensorRes + "f", event.values[0]));
                proximityStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_PRESSURE:
                mPressureLast = event.timestamp / 1000;
                metPressure.setText(String.format("%." + mPressureSensorRes + "f", event.values[0]));
                pressureStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_RELATIVE_HUMIDITY:
                mHumidityLast = event.timestamp / 1000;
                metHumid.setText(String.format("%." + mHumiditySensorRes + "f", event.values[0]));
                humidStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            case Sensor.TYPE_AMBIENT_TEMPERATURE:
                mTempLast = event.timestamp / 1000;
                metTemp.setText(String.format("%." + mTempSensorRes + "f", event.values[0]));
                tempStatus.setTextColor(getResources().getColor(accuracyToColor(event.accuracy)));
                break;
            }
        }
        if (isGpsViewReady && isRateElapsed) {
            switch (event.sensor.getType()) {
            case Sensor.TYPE_ORIENTATION:
                gpsStatusView.setYaw(event.values[0]);
                break;
            }
        }
    }

    /**
     * Called when preferences are changed.
     * 
     * This method processes changed to KEY_PREF_LOC_PROV, the list of selected
     * location providers. When called, it will unregister for all location 
     * updates and re-register for updates from the selected location providers.
     * (This includes unregistering and immediately re-registering for those
     * providers which remain selected  this is due to the fact that Android
     * does not support unregistering from a single location provider.) 
     */
    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        if (key.equals(SettingsActivity.KEY_PREF_LOC_PROV)) {
            // user selected or deselected location providers, refresh list
            registerLocationProviders(this);
            updateLocationProviders(this);
        }
    }

    /**
     * Called when a location provider's status changes. Does nothing.
     */
    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

    @Override
    protected void onStop() {
        isStopped = true;
        mLocationManager.removeUpdates(this);
        mLocationManager.removeGpsStatusListener(this);
        mSensorManager.unregisterListener(this);
        mTelephonyManager.listen(mPhoneStateListener, LISTEN_NONE);
        try {
            unregisterReceiver(mWifiScanReceiver);
        } catch (IllegalArgumentException e) {
            // sometimes the receiver isn't registered, make sure we don't crash
            Log.d(this.getLocalClassName(), "WifiScanReceiver was never registered, caught exception");
        }
        networkTimehandler.removeCallbacks(networkTimeRunnable);
        wifiTimehandler.removeCallbacks(wifiTimeRunnable);
        // we'll just skip that so locations will get invalidated in any case
        //providerInvalidationHandler.removeCallbacksAndMessages(null);
        super.onStop();
    }

    @Override
    public void onTabReselected(Tab tab, android.app.FragmentTransaction ft) {
        // probably ignore this event
    }

    @Override
    public void onTabSelected(Tab tab, android.app.FragmentTransaction ft) {
        // show the given tab
        // When the tab is selected, switch to the
        // corresponding page in the ViewPager.
        mViewPager.setCurrentItem(tab.getPosition());
    }

    @Override
    public void onTabUnselected(Tab tab, android.app.FragmentTransaction ft) {
        // hide the given tab (ignore this event)
    }

    /**
     * Registers for updates with selected location providers.
     * @param context
     */
    protected void registerLocationProviders(Context context) {
        Set<String> providers = new HashSet<String>(
                mSharedPreferences.getStringSet(SettingsActivity.KEY_PREF_LOC_PROV, new HashSet<String>(Arrays
                        .asList(new String[] { LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER }))));
        List<String> allProviders = mLocationManager.getAllProviders();

        mLocationManager.removeUpdates(this);

        ArrayList<String> removedProviders = new ArrayList<String>();
        for (String pr : providerLocations.keySet())
            if (!providers.contains(pr))
                removedProviders.add(pr);
        for (String pr : removedProviders)
            providerLocations.remove(pr);

        for (String pr : providers) {
            if (allProviders.indexOf(pr) >= 0) {
                if (!providerLocations.containsKey(pr)) {
                    Location location = new Location("");
                    providerLocations.put(pr, location);
                }
                if (!isStopped) {
                    mLocationManager.requestLocationUpdates(pr, 0, 0, this);
                    Log.d("MainActivity", "Registered with provider: " + pr);
                }
            } else {
                Log.w("MainActivity", "No " + pr
                        + " location provider found. Data display will not be available for this provider.");
            }
        }

        // if GPS is not selected, request location updates but don't store location
        if ((!providers.contains(LocationManager.GPS_PROVIDER)) && (!isStopped)
                && (allProviders.indexOf(LocationManager.GPS_PROVIDER) >= 0))
            mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
    }

    private void setEmbeddedTabs(Object actionBar, Boolean embed_tabs) {
        try {
            Method setHasEmbeddedTabsMethod = actionBar.getClass().getDeclaredMethod("setHasEmbeddedTabs",
                    boolean.class);
            setHasEmbeddedTabsMethod.setAccessible(true);
            setHasEmbeddedTabsMethod.invoke(actionBar, embed_tabs);
        } catch (Exception e) {
            Log.e("", "Error marking actionbar embedded", e);
        }
    }

    /**
     * Updates the list of cells in range.
     * <p>
     * This method is automatically called by
     * {@link PhoneStateListener#onCellInfoChanged(List)}
     * and {@link PhoneStateListener.onCellLocationChanged}. It must be called
     * manually whenever {@link #mCellsCdma}, {@link #mCellsGsm}, 
     * {@link #mCellsLte} or one of their values are modified, typically after
     * calling {@link android.telephony.TelephonyManager#getAllCellInfo()},
     * {@link android.telephony.TelephonyManager#getCellLocation()} or
     * {@link android.telephony.TelephonyManager#getNeighboringCellInfo()}. 
     */
    protected static void showCells() {
        if (!isRadioViewReady)
            return;

        int cdmaVisibility = View.GONE;
        int gsmVisibility = View.GONE;
        int lteVisibility = View.GONE;

        rilCells.removeAllViews();
        if (mCellsGsm.containsValue(mServingCell)) {
            showCellGsm((CellTowerGsm) mServingCell);
            gsmVisibility = View.VISIBLE;
        }
        for (CellTowerGsm cell : mCellsGsm.getAll())
            if (cell.hasSource() && (cell != mServingCell)) {
                showCellGsm(cell);
                gsmVisibility = View.VISIBLE;
            }
        rilGsmLayout.setVisibility(gsmVisibility);

        rilCdmaCells.removeAllViews();
        if (mCellsCdma.containsValue(mServingCell)) {
            showCellCdma((CellTowerCdma) mServingCell);
            cdmaVisibility = View.VISIBLE;
        }
        for (CellTowerCdma cell : mCellsCdma.getAll())
            if (cell.hasSource() && (cell != mServingCell)) {
                showCellCdma(cell);
                cdmaVisibility = View.VISIBLE;
            }
        rilCdmaLayout.setVisibility(cdmaVisibility);

        rilLteCells.removeAllViews();
        if (mCellsLte.containsValue(mServingCell)) {
            showCellLte((CellTowerLte) mServingCell);
            lteVisibility = View.VISIBLE;
        }
        for (CellTowerLte cell : mCellsLte.getAll())
            if (cell.hasSource() && (cell != mServingCell)) {
                showCellLte(cell);
                lteVisibility = View.VISIBLE;
            }
        rilLteLayout.setVisibility(lteVisibility);
    }

    protected static void showCellCdma(CellTowerCdma cell) {
        TableRow row = new TableRow(rilCdmaCells.getContext());
        row.setWeightSum(26);

        TextView newType = new TextView(rilCdmaCells.getContext());
        newType.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 2));
        newType.setTextAppearance(rilCdmaCells.getContext(), android.R.style.TextAppearance_Medium);
        newType.setTextColor(
                rilCdmaCells.getContext().getResources().getColor(getColorFromGeneration(cell.getGeneration())));
        newType.setText(rilCdmaCells.getContext().getResources().getString(R.string.smallDot));
        row.addView(newType);

        TextView newSid = new TextView(rilCdmaCells.getContext());
        newSid.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 6));
        newSid.setTextAppearance(rilCdmaCells.getContext(), android.R.style.TextAppearance_Medium);
        newSid.setText(formatCellData(rilCdmaCells.getContext(), null, cell.getSid()));
        row.addView(newSid);

        TextView newNid = new TextView(rilCdmaCells.getContext());
        newNid.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 5));
        newNid.setTextAppearance(rilCdmaCells.getContext(), android.R.style.TextAppearance_Medium);
        newNid.setText(formatCellData(rilCdmaCells.getContext(), null, cell.getNid()));
        row.addView(newNid);

        TextView newBsid = new TextView(rilCdmaCells.getContext());
        newBsid.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 9));
        newBsid.setTextAppearance(rilCdmaCells.getContext(), android.R.style.TextAppearance_Medium);
        newBsid.setText(formatCellData(rilCdmaCells.getContext(), null, cell.getBsid()));
        row.addView(newBsid);

        TextView newDbm = new TextView(rilCdmaCells.getContext());
        newDbm.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 4));
        newDbm.setTextAppearance(rilCdmaCells.getContext(), android.R.style.TextAppearance_Medium);
        newDbm.setText(formatCellDbm(rilCdmaCells.getContext(), null, cell.getDbm()));
        row.addView(newDbm);

        rilCdmaCells.addView(row,
                new TableLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    }

    protected static void showCellGsm(CellTowerGsm cell) {
        TableRow row = new TableRow(rilCells.getContext());
        row.setWeightSum(29);

        TextView newType = new TextView(rilCells.getContext());
        newType.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 2));
        newType.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newType.setTextColor(
                rilCells.getContext().getResources().getColor(getColorFromGeneration(cell.getGeneration())));
        newType.setText(rilCells.getContext().getResources().getString(R.string.smallDot));
        row.addView(newType);

        TextView newMcc = new TextView(rilCells.getContext());
        newMcc.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newMcc.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newMcc.setText(formatCellData(rilCells.getContext(), "%03d", cell.getMcc()));
        row.addView(newMcc);

        TextView newMnc = new TextView(rilCells.getContext());
        newMnc.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newMnc.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newMnc.setText(formatCellData(rilCells.getContext(), "%02d", cell.getMnc()));
        row.addView(newMnc);

        TextView newLac = new TextView(rilCells.getContext());
        newLac.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 5));
        newLac.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newLac.setText(formatCellData(rilCells.getContext(), null, cell.getLac()));
        row.addView(newLac);

        TextView newCid = new TextView(rilCells.getContext());
        newCid.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 9));
        newCid.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newCid.setText(formatCellData(rilCells.getContext(), null, cell.getCid()));
        row.addView(newCid);

        TextView newPsc = new TextView(rilCells.getContext());
        newPsc.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newPsc.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newPsc.setText(formatCellData(rilCells.getContext(), null, cell.getPsc()));
        row.addView(newPsc);

        TextView newDbm = new TextView(rilCells.getContext());
        newDbm.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 4));
        newDbm.setTextAppearance(rilCells.getContext(), android.R.style.TextAppearance_Medium);
        newDbm.setText(formatCellDbm(rilCells.getContext(), null, cell.getDbm()));
        row.addView(newDbm);

        rilCells.addView(row, new TableLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    }

    protected static void showCellLte(CellTowerLte cell) {
        TableRow row = new TableRow(rilLteCells.getContext());
        row.setWeightSum(29);

        TextView newType = new TextView(rilLteCells.getContext());
        newType.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 2));
        newType.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newType.setTextColor(
                rilLteCells.getContext().getResources().getColor(getColorFromGeneration(cell.getGeneration())));
        newType.setText(rilLteCells.getContext().getResources().getString(R.string.smallDot));
        row.addView(newType);

        TextView newMcc = new TextView(rilLteCells.getContext());
        newMcc.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newMcc.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newMcc.setText(formatCellData(rilLteCells.getContext(), "%03d", cell.getMcc()));
        row.addView(newMcc);

        TextView newMnc = new TextView(rilLteCells.getContext());
        newMnc.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newMnc.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newMnc.setText(formatCellData(rilLteCells.getContext(), "%02d", cell.getMnc()));
        row.addView(newMnc);

        TextView newTac = new TextView(rilLteCells.getContext());
        newTac.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 5));
        newTac.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newTac.setText(formatCellData(rilLteCells.getContext(), null, cell.getTac()));
        row.addView(newTac);

        TextView newCi = new TextView(rilLteCells.getContext());
        newCi.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 9));
        newCi.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newCi.setText(formatCellData(rilLteCells.getContext(), null, cell.getCi()));
        row.addView(newCi);

        TextView newPci = new TextView(rilLteCells.getContext());
        newPci.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 3));
        newPci.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newPci.setText(formatCellData(rilLteCells.getContext(), null, cell.getPci()));
        row.addView(newPci);

        TextView newDbm = new TextView(rilLteCells.getContext());
        newDbm.setLayoutParams(new TableRow.LayoutParams(0, LayoutParams.WRAP_CONTENT, 4));
        newDbm.setTextAppearance(rilLteCells.getContext(), android.R.style.TextAppearance_Medium);
        newDbm.setText(formatCellDbm(rilLteCells.getContext(), null, cell.getDbm()));
        row.addView(newDbm);

        rilLteCells.addView(row,
                new TableLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    }

    /**
     * Updates internal data structures when the user's selection of location providers has changed.
     * @param context
     */
    protected static void updateLocationProviders(Context context) {
        // add overlays
        if (isMapViewReady) {
            Set<String> providers = mSharedPreferences.getStringSet(SettingsActivity.KEY_PREF_LOC_PROV,
                    new HashSet<String>(Arrays.asList(
                            new String[] { LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER })));

            updateLocationProviderStyles();

            mapCircles = new HashMap<String, Circle>();
            mapMarkers = new HashMap<String, Marker>();

            ArrayList<String> removedProviders = new ArrayList<String>();
            for (String pr : providerInvalidators.keySet())
                if (!providers.contains(pr))
                    removedProviders.add(pr);
            for (String pr : removedProviders)
                providerInvalidators.remove(pr);

            Log.d("MainActivity", "Provider location cache: " + providerLocations.keySet().toString());

            Layers layers = mapMap.getLayerManager().getLayers();

            // remove all layers other than tile render layer from map
            for (int i = 0; i < layers.size();)
                if ((layers.get(i) instanceof TileRendererLayer) || (layers.get(i) instanceof TileDownloadLayer)) {
                    i++;
                } else {
                    layers.remove(i);
                }

            for (String pr : providers) {
                // no invalidator for GPS, which is invalidated through GPS status
                if ((!pr.equals(LocationManager.GPS_PROVIDER)) && (providerInvalidators.get(pr)) == null) {
                    final String provider = pr;
                    final Context ctx = context;
                    providerInvalidators.put(pr, new Runnable() {
                        private String mProvider = provider;

                        @Override
                        public void run() {
                            if (isMapViewReady) {
                                Location location = providerLocations.get(mProvider);
                                if (location != null)
                                    markLocationAsStale(location);
                                applyLocationProviderStyle(ctx, mProvider, LOCATION_PROVIDER_GRAY);
                            }
                        }
                    });
                }

                String styleName = assignLocationProviderStyle(pr);
                LatLong latLong;
                float acc;
                boolean visible;
                if ((providerLocations.get(pr) != null) && (providerLocations.get(pr).getProvider() != "")) {
                    latLong = new LatLong(providerLocations.get(pr).getLatitude(),
                            providerLocations.get(pr).getLongitude());
                    if (providerLocations.get(pr).hasAccuracy())
                        acc = providerLocations.get(pr).getAccuracy();
                    else
                        acc = 0;
                    visible = true;
                    if (isLocationStale(providerLocations.get(pr)))
                        styleName = LOCATION_PROVIDER_GRAY;
                    Log.d("MainActivity", pr + " has " + latLong.toString());
                } else {
                    latLong = new LatLong(0, 0);
                    acc = 0;
                    visible = false;
                    Log.d("MainActivity", pr + " has no location, hiding");
                }

                // Circle layer
                Resources res = context.getResources();
                TypedArray style = res
                        .obtainTypedArray(res.getIdentifier(styleName, "array", context.getPackageName()));
                Paint fill = AndroidGraphicFactory.INSTANCE.createPaint();
                fill.setColor(style.getColor(STYLE_FILL, R.color.circle_gray_fill));
                fill.setStyle(Style.FILL);
                Paint stroke = AndroidGraphicFactory.INSTANCE.createPaint();
                stroke.setColor(style.getColor(STYLE_STROKE, R.color.circle_gray_stroke));
                stroke.setStrokeWidth(4); // FIXME: make this DPI-dependent
                stroke.setStyle(Style.STROKE);
                Circle circle = new Circle(latLong, acc, fill, stroke);
                mapCircles.put(pr, circle);
                layers.add(circle);
                circle.setVisible(visible);

                // Marker layer
                Drawable drawable = style.getDrawable(STYLE_MARKER);
                Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable);
                Marker marker = new Marker(latLong, bitmap, 0, -bitmap.getHeight() * 9 / 20);
                mapMarkers.put(pr, marker);
                layers.add(marker);
                marker.setVisible(visible);
                style.recycle();
            }

            // move layers into view
            updateMap();
        }
    }

    /**
     * Updates the list of styles to use for the location providers.
     * 
     * This method updates the internal list of styles to use for displaying
     * locations on the map, assigning a style to each location provider.
     * Styles that are defined in {@link SharedPreferences} are preserved. If
     * none are defined, the GPS location provider is assigned the red style
     * and the network location provider is assigned the blue style. The
     * passive location provider is not assigned a style, as it does not send
     * any locations of its own. Other location providers are assigned one of
     * the following styles: green, orange, purple. If there are more location
     * providers than styles, the same style (including red and blue) can be
     * assigned to multiple providers. The mapping is written to 
     * SharedPreferences so that it will be preserved even as available
     * location providers change.
     */
    public static void updateLocationProviderStyles() {
        //FIXME: move code into assignLocationProviderStyle and use that
        List<String> allProviders = mLocationManager.getAllProviders();
        allProviders.remove(LocationManager.PASSIVE_PROVIDER);
        if (allProviders.contains(LocationManager.GPS_PROVIDER)) {
            providerStyles.put(LocationManager.GPS_PROVIDER,
                    mSharedPreferences.getString(
                            SettingsActivity.KEY_PREF_LOC_PROV_STYLE + LocationManager.GPS_PROVIDER,
                            LOCATION_PROVIDER_RED));
            mAvailableProviderStyles.remove(LOCATION_PROVIDER_RED);
            allProviders.remove(LocationManager.GPS_PROVIDER);
        }
        if (allProviders.contains(LocationManager.NETWORK_PROVIDER)) {
            providerStyles.put(LocationManager.NETWORK_PROVIDER,
                    mSharedPreferences.getString(
                            SettingsActivity.KEY_PREF_LOC_PROV_STYLE + LocationManager.NETWORK_PROVIDER,
                            LOCATION_PROVIDER_BLUE));
            mAvailableProviderStyles.remove(LOCATION_PROVIDER_BLUE);
            allProviders.remove(LocationManager.NETWORK_PROVIDER);
        }
        for (String prov : allProviders) {
            if (mAvailableProviderStyles.isEmpty())
                mAvailableProviderStyles.addAll(Arrays.asList(LOCATION_PROVIDER_STYLES));
            providerStyles.put(prov, mSharedPreferences.getString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + prov,
                    mAvailableProviderStyles.get(0)));
            mAvailableProviderStyles.remove(providerStyles.get(prov));
        }
        ;
        SharedPreferences.Editor spEditor = mSharedPreferences.edit();
        for (String prov : providerStyles.keySet())
            spEditor.putString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + prov, providerStyles.get(prov));
        spEditor.commit();
    }

    /**
     * Updates the map view so that all markers are visible.
     */
    public static void updateMap() {
        boolean needsRedraw = false;
        Dimension dimension = mapMap.getModel().mapViewDimension.getDimension();
        // just trigger a redraw if we're not going to pan or zoom
        if ((dimension == null) || (!isMapViewAttached)) {
            mapMap.getLayerManager().redrawLayers();
            return;
        }
        // move locations into view and zoom out as needed
        int tileSize = mapMap.getModel().displayModel.getTileSize();
        BoundingBox bb = null;
        BoundingBox bb2 = null;
        for (Location l : providerLocations.values())
            if ((l != null) && (l.getProvider() != "")) {
                double lat = l.getLatitude();
                double lon = l.getLongitude();
                double yRadius = l.hasAccuracy() ? ((l.getAccuracy() * 360.0f) / EARTH_CIRCUMFERENCE) : 0;
                double xRadius = l.hasAccuracy() ? (yRadius * Math.abs(Math.cos(lat))) : 0;

                double minLon = Math.max(lon - xRadius, -180);
                double maxLon = Math.min(lon + xRadius, 180);
                double minLat = Math.max(lat - yRadius, -90);
                double maxLat = Math.min(lat + yRadius, 90);

                if (!isLocationStale(l)) {
                    // location is up to date, add to main BoundingBox
                    if (bb != null) {
                        minLat = Math.min(bb.minLatitude, minLat);
                        maxLat = Math.max(bb.maxLatitude, maxLat);
                        minLon = Math.min(bb.minLongitude, minLon);
                        maxLon = Math.max(bb.maxLongitude, maxLon);
                    }
                    bb = new BoundingBox(minLat, minLon, maxLat, maxLon);
                } else {
                    // location is stale, add to stale BoundingBox
                    if (bb2 != null) {
                        minLat = Math.min(bb2.minLatitude, minLat);
                        maxLat = Math.max(bb2.maxLatitude, maxLat);
                        minLon = Math.min(bb2.minLongitude, minLon);
                        maxLon = Math.max(bb2.maxLongitude, maxLon);
                    }
                    bb2 = new BoundingBox(minLat, minLon, maxLat, maxLon);
                }
            }
        if (bb == null)
            bb = bb2; // all locations are stale, center to them
        if (bb == null) {
            needsRedraw = true;
        } else {
            byte newZoom = LatLongUtils.zoomForBounds(dimension, bb, tileSize);
            if (newZoom < mapMap.getModel().mapViewPosition.getZoomLevel()) {
                mapMap.getModel().mapViewPosition.setZoomLevel(newZoom);
            } else {
                needsRedraw = true;
            }

            MapViewProjection proj = new MapViewProjection(mapMap);
            Point nw = proj.toPixels(new LatLong(bb.maxLatitude, bb.minLongitude));
            Point se = proj.toPixels(new LatLong(bb.minLatitude, bb.maxLongitude));

            // move only if bb is not entirely visible
            if ((nw.x < 0) || (nw.y < 0) || (se.x > dimension.width) || (se.y > dimension.height)) {
                mapMap.getModel().mapViewPosition.setCenter(bb.getCenterPoint());
            } else {
                needsRedraw = true;
            }
        }
        if (needsRedraw)
            mapMap.getLayerManager().redrawLayers();
    }

    /**
     * Requeries neighboring cells
     */
    protected static void updateNeighboringCellInfo() {
        // this may not be supported on some devices (returns no data)
        String networkOperator = mTelephonyManager.getNetworkOperator();
        List<NeighboringCellInfo> neighboringCells = mTelephonyManager.getNeighboringCellInfo();
        mCellsGsm.updateAll(networkOperator, neighboringCells);
        mCellsLte.updateAll(networkOperator, neighboringCells);
    }

    /**
     * A {@link FragmentPagerAdapter} that returns a fragment corresponding to
     * one of the sections/tabs/pages.
     */
    public class SectionsPagerAdapter extends FragmentPagerAdapter {

        public SectionsPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            // getItem is called to instantiate the fragment for the given page.
            // Return a DummySectionFragment (defined as a static inner class
            // below) with the page number as its lone argument.
            Fragment fragment;
            switch (position) {
            case 0:
                fragment = new GpsSectionFragment();
                return fragment;
            case 1:
                fragment = new SensorSectionFragment();
                return fragment;
            case 2:
                fragment = new RadioSectionFragment();
                return fragment;
            case 3:
                fragment = new MapSectionFragment();
                return fragment;
            }
            return null;
        }

        @Override
        public int getCount() {
            // Show 4 total pages.
            return 4;
        }

        public Drawable getPageIcon(int position) {
            switch (position) {
            case 0:
                return getResources().getDrawable(R.drawable.ic_action_gps);
            case 1:
                return getResources().getDrawable(R.drawable.ic_action_sensor);
            case 2:
                return getResources().getDrawable(R.drawable.ic_action_radio);
            case 3:
                return getResources().getDrawable(R.drawable.ic_action_map);
            }
            return null;
        }

        @Override
        public CharSequence getPageTitle(int position) {
            Locale l = Locale.getDefault();
            switch (position) {
            case 0:
                return getString(R.string.title_section1).toUpperCase(l);
            case 1:
                return getString(R.string.title_section2).toUpperCase(l);
            case 2:
                return getString(R.string.title_section3).toUpperCase(l);
            case 3:
                return getString(R.string.title_section4).toUpperCase(l);
            }
            return null;
        }
    }

    /**
     * The fragment which displays GPS data.
     */
    public static class GpsSectionFragment extends Fragment {
        /**
         * The fragment argument representing the section number for this
         * fragment.
         */
        public static final String ARG_SECTION_NUMBER = "section_number";

        public GpsSectionFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main_gps, container, false);

            // Initialize controls
            gpsRootLayout = (LinearLayout) rootView.findViewById(R.id.gpsRootLayout);
            gpsSnrView = (GpsSnrView) rootView.findViewById(R.id.gpsSnrView);
            gpsStatusView = new GpsStatusView(rootView.getContext());
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                    LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL;
            params.weight = 1;
            gpsRootLayout.addView(gpsStatusView, 0, params);
            gpsLat = (TextView) rootView.findViewById(R.id.gpsLat);
            gpsLon = (TextView) rootView.findViewById(R.id.gpsLon);
            orDeclination = (TextView) rootView.findViewById(R.id.orDeclination);
            gpsSpeed = (TextView) rootView.findViewById(R.id.gpsSpeed);
            gpsAlt = (TextView) rootView.findViewById(R.id.gpsAlt);
            gpsTime = (TextView) rootView.findViewById(R.id.gpsTime);
            gpsBearing = (TextView) rootView.findViewById(R.id.gpsBearing);
            gpsAccuracy = (TextView) rootView.findViewById(R.id.gpsAccuracy);
            gpsOrientation = (TextView) rootView.findViewById(R.id.gpsOrientation);
            gpsSats = (TextView) rootView.findViewById(R.id.gpsSats);
            gpsTtff = (TextView) rootView.findViewById(R.id.gpsTtff);

            isGpsViewReady = true;

            return rootView;
        }

        @Override
        public void onDestroyView() {
            super.onDestroyView();
            isGpsViewReady = false;
        }
    }

    /**
     * The fragment which displays sensor data.
     */
    public static class SensorSectionFragment extends Fragment {
        /**
         * The fragment argument representing the section number for this
         * fragment.
         */
        public static final String ARG_SECTION_NUMBER = "section_number";

        public SensorSectionFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main_sensors, container, false);

            // Initialize controls
            accStatus = (TextView) rootView.findViewById(R.id.accStatus);
            accHeader = (TextView) rootView.findViewById(R.id.accHeader);
            accX = (TextView) rootView.findViewById(R.id.accX);
            accY = (TextView) rootView.findViewById(R.id.accY);
            accZ = (TextView) rootView.findViewById(R.id.accZ);
            accTotal = (TextView) rootView.findViewById(R.id.accTotal);
            rotStatus = (TextView) rootView.findViewById(R.id.rotStatus);
            rotHeader = (TextView) rootView.findViewById(R.id.rotHeader);
            rotX = (TextView) rootView.findViewById(R.id.rotX);
            rotY = (TextView) rootView.findViewById(R.id.rotY);
            rotZ = (TextView) rootView.findViewById(R.id.rotZ);
            rotTotal = (TextView) rootView.findViewById(R.id.rotTotal);
            magStatus = (TextView) rootView.findViewById(R.id.magStatus);
            magHeader = (TextView) rootView.findViewById(R.id.magHeader);
            magX = (TextView) rootView.findViewById(R.id.magX);
            magY = (TextView) rootView.findViewById(R.id.magY);
            magZ = (TextView) rootView.findViewById(R.id.magZ);
            magTotal = (TextView) rootView.findViewById(R.id.magTotal);
            orStatus = (TextView) rootView.findViewById(R.id.orStatus);
            orHeader = (TextView) rootView.findViewById(R.id.orHeader);
            orAzimuth = (TextView) rootView.findViewById(R.id.orAzimuth);
            orAziText = (TextView) rootView.findViewById(R.id.orAziText);
            orPitch = (TextView) rootView.findViewById(R.id.orPitch);
            orRoll = (TextView) rootView.findViewById(R.id.orRoll);
            miscHeader = (TextView) rootView.findViewById(R.id.miscHeader);
            tempStatus = (TextView) rootView.findViewById(R.id.tempStatus);
            tempHeader = (TextView) rootView.findViewById(R.id.tempHeader);
            metTemp = (TextView) rootView.findViewById(R.id.metTemp);
            pressureStatus = (TextView) rootView.findViewById(R.id.pressureStatus);
            pressureHeader = (TextView) rootView.findViewById(R.id.pressureHeader);
            metPressure = (TextView) rootView.findViewById(R.id.metPressure);
            humidStatus = (TextView) rootView.findViewById(R.id.humidStatus);
            humidHeader = (TextView) rootView.findViewById(R.id.humidHeader);
            metHumid = (TextView) rootView.findViewById(R.id.metHumid);
            lightStatus = (TextView) rootView.findViewById(R.id.lightStatus);
            lightHeader = (TextView) rootView.findViewById(R.id.lightHeader);
            light = (TextView) rootView.findViewById(R.id.light);
            proximityStatus = (TextView) rootView.findViewById(R.id.proximityStatus);
            proximityHeader = (TextView) rootView.findViewById(R.id.proximityHeader);
            proximity = (TextView) rootView.findViewById(R.id.proximity);

            isSensorViewReady = true;

            return rootView;
        }

        @Override
        public void onDestroyView() {
            super.onDestroyView();
            isSensorViewReady = false;
        }
    }

    /**
     * The fragment which displays radio network data.
     */
    public static class RadioSectionFragment extends Fragment {
        /**
         * The fragment argument representing the section number for this
         * fragment.
         */
        public static final String ARG_SECTION_NUMBER = "section_number";

        public RadioSectionFragment() {
        }

        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main_radio, container, false);

            // Initialize controls
            rilGsmLayout = (LinearLayout) rootView.findViewById(R.id.rilGsmLayout);
            rilCells = (TableLayout) rootView.findViewById(R.id.rilCells);

            rilCdmaLayout = (LinearLayout) rootView.findViewById(R.id.rilCdmaLayout);
            rilCdmaCells = (TableLayout) rootView.findViewById(R.id.rilCdmaCells);

            rilLteLayout = (LinearLayout) rootView.findViewById(R.id.rilLteLayout);
            rilLteCells = (TableLayout) rootView.findViewById(R.id.rilLteCells);

            wifiAps = (LinearLayout) rootView.findViewById(R.id.wifiAps);

            rilGsmLayout.setVisibility(View.GONE);
            rilCdmaLayout.setVisibility(View.GONE);
            rilLteLayout.setVisibility(View.GONE);

            isRadioViewReady = true;

            //get current phone info (first update won't fire until the cell actually changes)
            mCellsGsm.remove(CellTower.SOURCE_CELL_LOCATION);
            mCellsCdma.remove(CellTower.SOURCE_CELL_LOCATION);
            mCellsLte.remove(CellTower.SOURCE_CELL_LOCATION);
            String networkOperator = mTelephonyManager.getNetworkOperator();

            updateNeighboringCellInfo();

            // Requires API level 17. Many phones don't implement this method
            // at all and will return null, the ones that do implement it
            // may return only certain cell types.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                List<CellInfo> allCells = mTelephonyManager.getAllCellInfo();
                mCellsGsm.updateAll(allCells);
                mCellsCdma.updateAll(allCells);
                mCellsLte.updateAll(allCells);
            }

            CellLocation cellLocation = mTelephonyManager.getCellLocation();
            if (cellLocation instanceof CdmaCellLocation)
                mServingCell = mCellsCdma.update((CdmaCellLocation) cellLocation);
            else if (cellLocation instanceof GsmCellLocation) {
                CellTower newServingCell = getServingCell(new CellTowerList[] { mCellsGsm, mCellsLte });
                if (newServingCell == null) {
                    if (!mCellsLte.isEmpty()) {
                        Log.d("MainActivity",
                                "Trying to guess network type of GsmCellLocation... LTE cells found, assuming LTE");
                        newServingCell = mCellsLte.update(networkOperator, (GsmCellLocation) cellLocation);
                    } else {
                        Log.d("MainActivity",
                                "Trying to guess network type of GsmCellLocation... no LTE cells found, assuming GSM or UMTS");
                        newServingCell = mCellsGsm.update(networkOperator, (GsmCellLocation) cellLocation);
                    }
                    Log.d("MainActivity", String.format("newServingCell = %s, generation = %d",
                            newServingCell.getText(), newServingCell.getGeneration()));
                }
                mServingCell = newServingCell;
            }

            showCells();

            mWifiManager.startScan();

            return rootView;
        }

        @Override
        public void onDestroyView() {
            super.onDestroyView();
            isRadioViewReady = false;
        }
    }

    /**
     * The fragment which displays the map view.
     */
    public static class MapSectionFragment extends Fragment {
        /**
         * The fragment argument representing the section number for this
         * fragment.
         */
        public static final String ARG_SECTION_NUMBER = "section_number";

        public MapSectionFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main_map, container, false);

            mapReattach = (ImageButton) rootView.findViewById(R.id.mapReattach);

            mapReattach.setVisibility(View.GONE);
            isMapViewAttached = true;

            OnClickListener clis = new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (v == mapReattach) {
                        isMapViewAttached = true;
                        if (isMapViewReady) {
                            mapReattach.setVisibility(View.GONE);
                            updateMap();
                        }
                    }
                }
            };
            mapReattach.setOnClickListener(clis);

            // Initialize controls
            mapMap = new MapView(rootView.getContext());
            ((FrameLayout) rootView).addView(mapMap, 0);

            mapMap.setClickable(true);
            mapMap.getMapScaleBar().setVisible(true);
            mapMap.setBuiltInZoomControls(true);
            mapMap.getMapZoomControls().setZoomLevelMin((byte) 10);
            mapMap.getMapZoomControls().setZoomLevelMax((byte) 20);

            if (mapTileCache == null)
                mapTileCache = PersistentTileCache.createTileCache(rootView.getContext(), "MapQuest",
                        mapMap.getModel().displayModel.getTileSize(), 1f,
                        mapMap.getModel().frameBufferModel.getOverdrawFactor());

            LayerManager layerManager = mapMap.getLayerManager();
            Layers layers = layerManager.getLayers();
            layers.clear();

            float lat = mSharedPreferences.getFloat(SettingsActivity.KEY_PREF_MAP_LAT, 360.0f);
            float lon = mSharedPreferences.getFloat(SettingsActivity.KEY_PREF_MAP_LON, 360.0f);

            if ((lat < 360.0f) && (lon < 360.0f)) {
                mapMap.getModel().mapViewPosition.setCenter(new LatLong(lat, lon));
            }

            int zoom = mSharedPreferences.getInt(SettingsActivity.KEY_PREF_MAP_ZOOM, 16);
            mapMap.getModel().mapViewPosition.setZoomLevel((byte) zoom);

            /*
            TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache,
              mapMap.getModel().mapViewPosition, false, AndroidGraphicFactory.INSTANCE);
                
            //FIXME: have user select map file
            tileRendererLayer.setMapFile(new File(Environment.getExternalStorageDirectory(), "org.openbmap/maps/germany.map"));
                
            tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER);
                
            //tileRendererLayer.setTextScale(1.5f);
            layers.add(tileRendererLayer);
            */

            OnlineTileSource onlineTileSource = new OnlineTileSource(
                    new String[] { "otile1.mqcdn.com", "otile2.mqcdn.com", "otile3.mqcdn.com", "otile4.mqcdn.com" },
                    80);
            onlineTileSource.setName("MapQuest").setAlpha(false).setBaseUrl("/tiles/1.0.0/map/").setExtension("png")
                    .setParallelRequestsLimit(8).setProtocol("http").setTileSize(256).setZoomLevelMax((byte) 18)
                    .setZoomLevelMin((byte) 0);

            mapDownloadLayer = new TileDownloadLayer(mapTileCache, mapMap.getModel().mapViewPosition,
                    onlineTileSource, AndroidGraphicFactory.INSTANCE);
            layers.add(mapDownloadLayer);
            mapDownloadLayer.onResume();

            GestureDetector gd = new GestureDetector(rootView.getContext(),
                    new GestureDetector.SimpleOnGestureListener() {
                        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                            mapReattach.setVisibility(View.VISIBLE);
                            isMapViewAttached = false;
                            return false;
                        }
                    });

            mapMap.setGestureDetector(gd);

            isMapViewReady = true;

            //parse list of location providers
            updateLocationProviders(rootView.getContext());

            return rootView;
        }

        @Override
        public void onDestroyView() {
            LatLong center = mapMap.getModel().mapViewPosition.getCenter();
            byte zoom = mapMap.getModel().mapViewPosition.getZoomLevel();

            SharedPreferences.Editor spEditor = mSharedPreferences.edit();
            spEditor.putFloat(SettingsActivity.KEY_PREF_MAP_LAT, (float) center.latitude);
            spEditor.putFloat(SettingsActivity.KEY_PREF_MAP_LON, (float) center.longitude);
            spEditor.putInt(SettingsActivity.KEY_PREF_MAP_ZOOM, zoom);
            spEditor.commit();

            super.onDestroyView();
            isMapViewReady = false;
        }
    }
}