com.landenlabs.all_devtool.GpsFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.landenlabs.all_devtool.GpsFragment.java

Source

package com.landenlabs.all_devtool;

/*
 * Copyright (c) 2016 Dennis Lang (LanDen Labs) landenlabs@gmail.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @author Dennis Lang  (3/21/2015)
 * @see http://LanDenLabs.com/
 *
 */

import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.BaseExpandableListAdapter;
import android.widget.CheckBox;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.landenlabs.all_devtool.receivers.GpsReceiver;
import com.landenlabs.all_devtool.util.LLog;
import com.landenlabs.all_devtool.util.Ui;
import com.landenlabs.all_devtool.util.Utils;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Display "Gps"  information.
 *
 * @author Dennis Lang
 *
 */
public class GpsFragment extends DevFragment implements View.OnClickListener, LocationListener, GpsStatus.Listener,
        GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {

    // Logger - set to LLog.DBG to only log in Debug build, use LLog.On to always log.
    private final LLog m_log = LLog.DBG;

    public static String s_name = "GPS";
    SubMenu m_menu;

    static final int s_providersRow = 0;
    static final int s_lastUpdateRow = 1;
    static final int s_detailRow = 2;
    static final int s_maxRows = 3;

    private final ArrayList<GpsInfo> m_list = new ArrayList<GpsInfo>(s_maxRows);
    private final ItemList m_detailList = new ItemList();
    private ExpandableListView m_listView;
    private ImageView m_statusIcon;

    // Additional times
    private static final Locale s_locale = Locale.getDefault();
    private static final SimpleDateFormat s_hour12Format = new SimpleDateFormat("hh:mm:ss a", s_locale);
    private static final SimpleDateFormat s_hour24Format = new SimpleDateFormat("HH:mm:ss.S", s_locale);
    private static final SimpleDateFormat s_time12Format = new SimpleDateFormat("MMM-dd hh:mm a", s_locale);
    private static final SimpleDateFormat s_time24Format = new SimpleDateFormat("MMM-dd HH:mm", s_locale);

    private static SimpleDateFormat s_hourFormat = s_hour24Format;

    static final int s_colorGps = 0xff80ffff; // must match Status toggle
    static final int s_colorMsg = s_colorGps;
    static final int s_maxToKeep = 20;
    static final long s_noTime = -1;

    // ---- GPS ----
    // https://developers.google.com/android/reference/com/google/android/gms/location/LocationRequest
    private static final String TAG = "LocationActivity";
    // 1hr = power blame once per hour
    private static final long GPS_INTERVAL = 1000 * 60 * 60;

    // Update no faster then GPS_FASTEST_INTERVAL if GPS updates for other reasons or hits
    // the GPS_INTERVAL interval
    private static final long GPS_FASTEST_INTERVAL = 1000 * 1; // 1 sec
    private static final long GPS_SLOW_INTERVAL = 1000 * 60 * 5; // 5 min

    LocationManager m_locMgr;
    LocationRequest m_locationRequest;
    GoogleApiClient m_googleApiClient;

    class LocInfo {
        Location m_currLocation;
        Location m_prevLocation;
    }

    static final String STATUS_CB = "Status";
    static final String FUSED_PROVIDER = "fused";
    Map<String, LocInfo> m_mapLocProviders = new HashMap<String, LocInfo>();

    TextView m_gpsTv;
    boolean m_gpsMonitor = false;

    Map<String, CheckBox> m_providersCb = new HashMap<String, CheckBox>();
    SparseArray<CheckBox> m_colorsCb = new SparseArray<CheckBox>();
    Map<String, GpsItem> m_lastUpdates = new HashMap<String, GpsItem>();

    boolean m_isGpsFixed = false;
    boolean m_isGpsEnabled = false;
    IntentFilter m_intentFilter = new IntentFilter();
    private BroadcastReceiver m_gpsReceiver = new BroadcastReceiver() {
        public void onReceive(Context paramContext, Intent paramIntent) {
            String str = paramIntent.getAction();
            Log.d("foo", "gps receiver action=" + str + paramIntent);
            // if ((str.equals("android.location.GPS_ENABLED_CHANGE")) || (str.equals("android.location.GPS_FIX_CHANGE")))
            GpsFragment.this.updateGps(paramIntent);
        }
    };

    private void updateGps(Intent paramIntent) {
        String str = paramIntent.getAction();
        boolean isEnabled = paramIntent.getBooleanExtra("enabled", false);
        if ((str.equals("android.location.GPS_FIX_CHANGE"))) {
            m_isGpsFixed = isEnabled;
            addMsgToDetailRow(s_colorGps, "Gps " + (m_isGpsFixed ? "fixed" : "unfixed"));
        } else if ((str.equals("android.location.GPS_ENABLED_CHANGE"))) {
            m_isGpsEnabled = isEnabled;
            addMsgToDetailRow(s_colorGps, "Gps " + (m_isGpsEnabled ? "enabled" : "disabled"));
        }
        showProviders();
    }

    // ============================================================================================
    // DevFragment methods
    public static DevFragment create() {
        return new GpsFragment();
    }

    @Override
    public String getName() {
        return s_name;
    }

    @Override
    public List<Bitmap> getBitmaps(int maxHeight) {
        // List<Bitmap> bitmapList = new ArrayList<Bitmap>();
        // bitmapList.add(Utils.grabScreen(this.getActivity()));
        // return bitmapList;
        return Utils.getListViewAsBitmaps(m_listView, maxHeight);
    }

    @Override
    public void onSelected() {
        GlobalInfo.s_globalInfo.mainFragActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        GlobalInfo.s_globalInfo.mainFragActivity.getWindow()
                .addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    // ============================================================================================
    // Fragment methods
    @SuppressWarnings("deprecation")
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);

        View rootView = inflater.inflate(R.layout.gps_tab, container, false);
        m_statusIcon = Ui.viewById(rootView, R.id.gpsStatus);
        m_statusIcon.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
            }
        });

        Ui.viewById(rootView, R.id.gps_clear).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                clearList();
            }
        });

        m_list.clear();
        // ---- Get UI components  ----
        for (int idx = 0; idx != s_maxRows; idx++)
            m_list.add(null);
        m_list.set(s_providersRow, new GpsInfo(new GpsItem("Providers")));
        m_list.set(s_lastUpdateRow, new GpsInfo(new GpsItem("Last Update")));
        m_list.set(s_detailRow, new GpsInfo(new GpsItem("Detail History")));

        m_listView = Ui.viewById(rootView, R.id.gpsListView);
        final GpsArrayAdapter adapter = new GpsArrayAdapter(this.getActivity());
        m_listView.setAdapter(adapter);

        // ---- Setup GPS ----
        m_locMgr = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE);

        m_gpsTv = Ui.viewById(rootView, R.id.gps);
        if (isGooglePlayServicesAvailable()) {
            m_locationRequest = new LocationRequest();
            m_locationRequest.setInterval(GPS_INTERVAL);
            m_locationRequest.setFastestInterval(GPS_FASTEST_INTERVAL);
            // Priority needs to match permissions.
            //  Use LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY with
            // <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
            //  Use LocationRequest.PRIORITY_HIGH_ACCURACY with
            // <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
            m_locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);

            m_googleApiClient = new GoogleApiClient.Builder(this.getActivity()).addApi(LocationServices.API)
                    .addConnectionCallbacks(this).addOnConnectionFailedListener(this).build();

            m_gpsTv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    m_gpsMonitor = !m_gpsMonitor;
                    view.setSelected(m_gpsMonitor);
                    addMsgToDetailRow(s_colorMsg, m_gpsMonitor ? "Start Monitor" : "Stop Monitor");
                    showProviders();
                }
            });

        } else {
            m_gpsTv.setText("Need Google Play Service");
        }

        if (ActivityCompat.checkSelfPermission(getContext(),
                Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            m_locMgr.addGpsStatusListener(this);
        }

        /*  http://stackoverflow.com/questions/11398732/how-do-i-receive-the-system-broadcast-when-gps-status-has-changed
        <uses-permission android:name="android.permission.ACCESS_COARSE_UPDATES" />
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        <receiver android:name=".GpsReceiver">
        <intent-filter>
             <action android:name="android.location.GPS_ENABLED_CHANGE" />
             <action android:name="android.location.PROVIDERS_CHANGED" />
        </intent-filter>
        </receiver>
        */
        // GpsReceiver m_gpsReceiver = new GpsReceiver();
        m_intentFilter.addAction(GpsReceiver.GPS_ENABLED_CHANGE_ACTION);
        if (Build.VERSION.SDK_INT >= 19) {
            m_intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
        }
        m_intentFilter.addAction(GpsReceiver.GPS_FIX_CHANGE_ACTION);
        m_intentFilter.addAction(LocationManager.PROVIDERS_CHANGED_ACTION);

        showProviders();
        // TODO - get available providers
        getCheckBox(rootView, R.id.gpsFuseCb, FUSED_PROVIDER);
        getCheckBox(rootView, R.id.gpsGPSCb, LocationManager.GPS_PROVIDER);
        getCheckBox(rootView, R.id.gpsNetwkCb, LocationManager.NETWORK_PROVIDER);
        getCheckBox(rootView, R.id.gpsLowPwrCb, LocationManager.PASSIVE_PROVIDER);
        getCheckBox(rootView, R.id.gpsStatusCb, STATUS_CB);

        for (CheckBox cb : m_providersCb.values()) {
            cb.setOnClickListener(this);
        }
        initLastUpdate();

        // Expand All
        for (int idx = 0; idx != s_maxRows; idx++)
            m_listView.expandGroup(idx);

        return rootView;
    }

    @Override
    public void onResume() {
        super.onResume();
        startLocationUpdates();
        LocalBroadcastManager.getInstance(this.getActivity()).registerReceiver(m_gpsReceiver, m_intentFilter);
        showProviders();
    }

    @Override
    public void onPause() {
        super.onPause();
        stopLocationUpdates();
        LocalBroadcastManager.getInstance(this.getActivity()).unregisterReceiver(m_gpsReceiver);
    }

    @Override
    public void onStart() {
        super.onStart();
        m_googleApiClient.connect();
    }

    @Override
    public void onStop() {
        super.onStop();
        m_googleApiClient.disconnect();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int pos = -1;
        int id = item.getItemId();
        switch (id) {
        case R.id.clock_12:
            s_hourFormat = s_hour12Format;
            addMsgToDetailRow(s_colorMsg, "12h Clock");
            break;
        case R.id.clock_24:
            s_hourFormat = s_hour24Format;
            addMsgToDetailRow(s_colorMsg, "24h Clock");
            break;

        case R.id.gps_clear_list:
            clearList();
            break;

        case R.id.gps_fast_accurate:
            if (m_locationRequest != null) {
                m_locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
                m_locationRequest.setFastestInterval(GPS_FASTEST_INTERVAL);
                m_locationRequest.setSmallestDisplacement(0);
                addMsgToDetailRow(s_colorMsg, "1sec 0 miles HighPwr");
                startLocationUpdates();
            }
            break;

        case R.id.gps_fast_accurate_one_mile:
            if (m_locationRequest != null) {
                m_locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
                m_locationRequest.setFastestInterval(GPS_FASTEST_INTERVAL);
                m_locationRequest.setSmallestDisplacement(1609.0f); // 1 mile = 1609 km
                addMsgToDetailRow(s_colorMsg, "1sec 1 mile HighPwr");
                startLocationUpdates();
            }
            break;

        case R.id.gps_fast_balanced:
            if (m_locationRequest != null) {
                m_locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
                m_locationRequest.setFastestInterval(GPS_FASTEST_INTERVAL);
                m_locationRequest.setSmallestDisplacement(0);
                addMsgToDetailRow(s_colorMsg, "1sec 0 miles Balanced");
                startLocationUpdates();
            }
            break;
        case R.id.gps_fast_balanced_one_mile:
            if (m_locationRequest != null) {
                m_locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
                m_locationRequest.setFastestInterval(GPS_FASTEST_INTERVAL);
                m_locationRequest.setSmallestDisplacement(1609.0f); // 1 mile = 1609 km
                addMsgToDetailRow(s_colorMsg, "1sec 1 miles Balanced");
                startLocationUpdates();
            }
            break;
        case R.id.gps_slow_balanced_one_mile:
            if (m_locationRequest != null) {
                m_locationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
                m_locationRequest.setFastestInterval(GPS_SLOW_INTERVAL);
                m_locationRequest.setSmallestDisplacement(1609.0f); // 1 mile = 1609 km
                addMsgToDetailRow(s_colorMsg, "5min 1 miles Balanced");
                startLocationUpdates();
            }
            break;

        default:
            break;
        }

        item.setChecked(true);
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        m_menu = menu.addSubMenu("GPS Settings");
        inflater.inflate(R.menu.gps_menu, m_menu);
        m_menu.findItem(R.id.clock_12).setChecked(true);
    }

    // ============================================================================================
    //  View.OnClickListener
    @Override
    public void onClick(View view) {
        updateDetailRows();
    }

    GpsStatus gpsStatus = null;

    // ============================================================================================
    //  GpsStatus.Listener
    @Override
    public void onGpsStatusChanged(int event) {
        if (getActivity() != null) {
            final LocationManager locMgr = (LocationManager) getActivity()
                    .getSystemService(Context.LOCATION_SERVICE);

            try {
                gpsStatus = locMgr.getGpsStatus(gpsStatus);
                String msg = "";
                switch (event) {
                case GpsStatus.GPS_EVENT_STARTED:
                    msg = "GPS event started";
                    break;
                case GpsStatus.GPS_EVENT_STOPPED:
                    msg = "GPS event stopped";
                    break;
                case GpsStatus.GPS_EVENT_FIRST_FIX:
                    msg = "GPS first fix";
                    break;
                case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
                    msg = "GPS sat status";
                    break;
                }

                if (TextUtils.isEmpty(msg)) {
                    addMsgToDetailRow(s_colorGps, msg);
                    GpsItem gpsItem = m_lastUpdates.get(STATUS_CB);
                    if (gpsItem != null) {
                        gpsItem.set(System.currentTimeMillis(), msg);
                        listChanged();
                    }
                }
                showProviders();
            } catch (SecurityException ex) {
                Log.e(TAG, ex.getMessage());
            }
        }
    }

    // ============================================================================================
    // GoogleApiClient.ConnectionCallbacks
    @Override
    public void onConnected(Bundle bundle) {
        startLocationUpdates();
        addMsgToDetailRow(s_colorMsg, "GPS Started");
    }

    @Override
    public void onConnectionSuspended(int i) {
        addMsgToDetailRow(s_colorMsg, "GPS suspended");
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Ui.ToastBig(this.getActivity(), "GPS error\n" + connectionResult.describeContents());
        addMsgToDetailRow(s_colorMsg, "GPS error");
    }

    // ============================================================================================
    // LocationListener,
    @Override
    public void onLocationChanged(Location location) {

        boolean isDupLoc = false;

        try {
            Location gpsLoc = m_locMgr.getLastKnownLocation(LocationManager.GPS_PROVIDER);
            if (gpsLoc != null) {
                showGPS(gpsLoc);
                isDupLoc = isDupLoc || (location.distanceTo(gpsLoc) == 0);
            }
        } catch (Exception ex) {
            Toast.makeText(this.getActivity(), "GPS " + ex.getMessage(), Toast.LENGTH_LONG).show();
        }

        try {
            if (ActivityCompat.checkSelfPermission(getContext(),
                    Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                    && ActivityCompat.checkSelfPermission(getContext(),
                            Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {

                Location netLoc = m_locMgr.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
                if (netLoc != null) {
                    showGPS(netLoc);
                    isDupLoc = isDupLoc || (location.distanceTo(netLoc) == 0);
                }
            }
        } catch (Exception ex) {
            Toast.makeText(this.getActivity(), "GPS needs location permission\n" + ex.getMessage(),
                    Toast.LENGTH_LONG).show();
        }

        try {
            Location passiveLoc = m_locMgr.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
            if (null != passiveLoc) {
                showGPS(passiveLoc);
                isDupLoc = isDupLoc || (location.distanceTo(passiveLoc) == 0);
            }
        } catch (Exception ex) {
            Toast.makeText(this.getActivity(), "Passive " + ex.getMessage(), Toast.LENGTH_LONG).show();
        }

        if (!isDupLoc)
            showGPS(location);
    }

    // ============================================================================================
    // GpsFragment
    private CheckBox getCheckBox(View rootView, int resId, String provider) {
        CheckBox cb = Ui.viewById(rootView, resId);
        m_providersCb.put(provider, cb);
        m_colorsCb.put(cb.getCurrentTextColor(), cb);
        return cb;
    }

    private int getProviderColor(String provider) {
        CheckBox cb = m_providersCb.get(provider);
        int color = (cb == null) ? s_colorMsg : cb.getCurrentTextColor();
        return color;
    }

    private void showProviders() {
        GpsInfo gpsInfo = m_list.get(s_providersRow);
        ItemList itemList = gpsInfo.getList();
        itemList.clear();

        List<String> gpsProviders = m_locMgr.getAllProviders();
        int idx = 1;
        for (String providerName : gpsProviders) {
            if (ActivityCompat.checkSelfPermission(getContext(),
                    Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                    || ActivityCompat.checkSelfPermission(getContext(),
                            Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                try {
                    LocationProvider provider = m_locMgr.getProvider(providerName);
                    if (null != provider) {
                        int color = getProviderColor(providerName);
                        String msg = String.format("%-10s %3s Accuracy:%d Pwr:%d", providerName,
                                (m_locMgr.isProviderEnabled(providerName) ? "On" : "Off"), provider.getAccuracy(),
                                provider.getPowerRequirement());
                        itemList.add(new GpsItem(s_noTime, msg, color));
                    }
                } catch (SecurityException ex) {
                    m_log.e(ex.getLocalizedMessage());
                    m_gpsTv.setEnabled(false);
                    addMsgToDetailRow(s_colorMsg, "GPS not available");
                    addMsgToDetailRow(s_colorMsg, ex.getLocalizedMessage());
                }
            }
        }
        listChanged();

        if (m_locMgr.isProviderEnabled(LocationManager.GPS_PROVIDER))
            m_statusIcon.setImageResource(R.drawable.gps_satellite);
        else if (m_locMgr.isProviderEnabled(LocationManager.NETWORK_PROVIDER))
            m_statusIcon.setImageResource(R.drawable.gps_tower);
        else if (m_locMgr.isProviderEnabled(LocationManager.PASSIVE_PROVIDER))
            m_statusIcon.setImageResource(R.drawable.gps_passive);
        else
            m_statusIcon.setImageResource(R.drawable.gps_off);
    }

    private void initLastUpdate() {
        GpsInfo gpsInfo = m_list.get(s_lastUpdateRow);
        ItemList itemList = gpsInfo.getList();
        if (itemList.isEmpty()) {
            //          1234567 12345678 123456789 12345m
            //          hh:mm:ss pm 12.45678 123.56789 12345m network
            itemList.add(new GpsItem("Time Latitude Longitude Accuracy Provider"));

            for (String provider : m_providersCb.keySet()) {
                int color = getProviderColor(provider);
                GpsItem gpsItem = new GpsItem(s_noTime, provider, color);
                itemList.add(gpsItem);
                m_lastUpdates.put(provider, gpsItem);
            }
        }

        for (String provider : m_providersCb.keySet()) {
            boolean vis = m_providersCb.get(provider).isChecked();
            // m_lastUpdates.get(provider).setVisibility(vis ? View.VISIBLE : View.eE);
        }
    }

    private void startLocationUpdates() {
        if (m_googleApiClient.isConnected()) {
            stopLocationUpdates();

            if (ActivityCompat.checkSelfPermission(getContext(),
                    Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                    || ActivityCompat.checkSelfPermission(getContext(),
                            Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {

                PendingResult<Status> pendingResult = LocationServices.FusedLocationApi
                        .requestLocationUpdates(m_googleApiClient, m_locationRequest, this);
            }
        }
    }

    protected void stopLocationUpdates() {
        LocationServices.FusedLocationApi.removeLocationUpdates(m_googleApiClient, this);
    }

    private boolean isGooglePlayServicesAvailable() {
        int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this.getActivity());
        if (ConnectionResult.SUCCESS == status) {
            addMsgToDetailRow(s_colorMsg, "GooglePlay Available");
            return true;
        } else {
            String errMsg = GooglePlayServicesUtil.getErrorString(status);
            Ui.ShowMessage(this.getActivity(), errMsg);
            addMsgToDetailRow(s_colorMsg, "GooglePlay Unavailable");
            return false;
        }
    }

    private void clearList() {
        GpsInfo gpsInfo = m_list.get(s_detailRow);
        gpsInfo.getList().clear();
        listChanged();
    }

    private void showGPS(Location location) {
        if (location == null)
            return;

        String provider = location.getProvider();
        LocInfo locInfo = m_mapLocProviders.get(provider);
        if (locInfo == null) {
            locInfo = new LocInfo();
            m_mapLocProviders.put(provider, locInfo);
        }

        locInfo.m_prevLocation = locInfo.m_currLocation;
        locInfo.m_currLocation = location;

        if (null != location) {
            addLocToDetailRow(locInfo);

            String msg = String.format("%8.5f,%9.5f %5.0fm %s", location.getLatitude(), location.getLongitude(),
                    location.getAccuracy(), location.getProvider());
            GpsItem gpsItem = m_lastUpdates.get(provider);
            if (gpsItem != null) {
                gpsItem.set(location.getTime(), msg);
                listChanged();
            } else
                Ui.ShowMessage(this.getActivity(), "null text for " + provider); // DEBUG

        } else {
            m_gpsTv.setText("No location");
        }
    }

    private int addLocToDetailRow(LocInfo locInfo) {

        if (m_detailList.size() > s_maxToKeep) {
            m_detailList.remove(0);
        }

        Location prevLoc = locInfo.m_prevLocation;
        Location currLoc = locInfo.m_currLocation;
        double km = (prevLoc != null) ? Utils.kilometersBetweenLocations(prevLoc, currLoc) : 0;
        double feet = km * 3280.84;
        int color = m_providersCb.get(currLoc.getProvider()).getCurrentTextColor();

        String msg = String.format("%.5f,%.5f  %.0fm %.0ft %s", currLoc.getLatitude(), currLoc.getLongitude(),
                km * 1000, feet, currLoc.getProvider());
        m_log.i("GPS " + msg);
        GpsItem item = new GpsItem(currLoc.getTime(), msg, color);

        return updateDetailRow(item);
    }

    private int addMsgToDetailRow(int textColor, String msg) {

        if (textColor == s_colorMsg) {
            GpsItem gpsItem = m_lastUpdates.get(STATUS_CB);
            if (gpsItem != null) {
                gpsItem.set(System.currentTimeMillis(), msg);
                listChanged();
            }
        }

        if (m_detailList.size() > s_maxToKeep) {
            m_detailList.remove(0);
        }

        Date now = new Date();
        GpsItem item = new GpsItem(now.getTime(), msg, textColor);
        return updateDetailRow(item);
    }

    /**
     * Add filtered item to detail row.
     * @param item
     * @return
     */
    private int updateDetailRow(GpsItem item) {
        boolean isDup = false;
        int detailCnt = m_detailList.size();
        if (detailCnt > 0) {
            GpsItem prevItem = m_detailList.get(detailCnt - 1);
            if (prevItem.m_msg.equals(item.m_msg) && (item.m_time - prevItem.m_time) < 1000) {
                isDup = true;
            }
        }

        if (!isDup) {
            m_detailList.add(item);
        }

        GpsInfo gpsInfo = m_list.get(s_detailRow);
        ItemList itemList = gpsInfo.getList();
        CheckBox cb = m_colorsCb.get(item.m_color);
        // if (m_gpsMonitor && (cb == null || cb.isChecked())) {
        if (!isDup) {
            itemList.add(item);
            listChanged();
        }
        // }

        return itemList.size();
    }

    /**
     * Refill detail row with filtered items.
     * @return
     */
    private int updateDetailRows() {
        GpsInfo gpsInfo = m_list.get(s_detailRow);
        ItemList itemList = gpsInfo.getList();
        itemList.clear();

        for (GpsItem item : m_detailList) {
            CheckBox cb = m_colorsCb.get(item.m_color);
            if (m_gpsMonitor && (cb == null || cb.isChecked())) {
                itemList.add(item);
            }
        }
        listChanged();
        return itemList.size();
    }

    // ============================================================================================
    // Static Helper methods

    /**
     * Format time interval
     *
     * @param elapsedMillis
     * @return
     */
    private static String formatInterval(final long elapsedMillis) {
        final long day = TimeUnit.MICROSECONDS.toHours(elapsedMillis) / 24;
        final long hr = TimeUnit.MILLISECONDS.toHours(elapsedMillis) % 24;
        final long min = TimeUnit.MILLISECONDS.toMinutes(elapsedMillis) % 60;
        final long sec = TimeUnit.MILLISECONDS.toSeconds(elapsedMillis) % 60;
        final long ms = TimeUnit.MILLISECONDS.toMillis(elapsedMillis) % 1000;
        return String.format("%s %02d:%02d:%02d.%03d", (day == 0 ? "" : String.valueOf(day)), hr, min, sec, ms);
    }

    // ============================================================================================
    // Expanded List Adapter

    private void listChanged() {
        ((BaseExpandableListAdapter) m_listView.getExpandableListAdapter()).notifyDataSetChanged();
    }

    static class GpsItem {
        static final int s_white = 0xffffffff;
        long m_time;
        String m_msg;
        int m_color;

        public GpsItem() {
            m_time = s_noTime;
            m_msg = null;
            m_color = s_white;
        }

        public GpsItem(String msg) {
            m_time = s_noTime;
            m_msg = msg;
            m_color = s_white;
        }

        public GpsItem(String msg, int color) {
            m_time = s_noTime;
            m_msg = msg;
            m_color = color;
        }

        public GpsItem(long time, String msg, int color) {
            m_time = time;
            m_msg = msg;
            m_color = color;
        }

        public void set(long time, String msg) {
            m_time = time;
            m_msg = msg;
        }
    }

    class ItemList extends ArrayList<GpsItem> {
    }

    class GpsInfo {
        GpsItem m_item;
        ItemList m_itemList;

        GpsInfo() {
            m_item = null;
            m_itemList = null;
        }

        GpsInfo(GpsItem item) {
            m_item = item;
            m_itemList = new ItemList();
        }

        public String toString() {
            return m_item.m_msg;
        }

        public GpsItem getItem() {
            return m_item;
        }

        public ItemList getList() {
            return m_itemList;
        }

        public int getCount() {
            return (m_itemList == null) ? 0 : m_itemList.size();
        }

    }

    final static int EXPANDED_LAYOUT = R.layout.gps_list_detail_row;
    final static int SUMMARY_LAYOUT = R.layout.gps_list_group_row;

    /**
     * ExpandableLis UI 'data model' class
     */
    private class GpsArrayAdapter extends BaseExpandableListAdapter {
        private final LayoutInflater m_inflater;

        public GpsArrayAdapter(Context context) {
            m_inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        /**
         * Generated expanded detail view object.
         */
        @Override
        public View getChildView(final int groupPosition, final int childPosition, boolean isLastChild,
                View convertView, ViewGroup parent) {

            GpsInfo gpsInfoParent = m_list.get(groupPosition);
            GpsItem gpsInfoChild = gpsInfoParent.getList().get(childPosition);

            RelativeLayout expandView = (RelativeLayout) convertView;
            // if (null == expandView) {
            expandView = (RelativeLayout) m_inflater.inflate(EXPANDED_LAYOUT, parent, false);
            // }

            long time = gpsInfoChild.m_time;
            String timeStr = (time != s_noTime) ? s_hourFormat.format(time) + " " : "";
            String msg = timeStr + gpsInfoChild.m_msg;

            TextView textView = Ui.viewById(expandView, R.id.gpsColumn);
            textView.setText(msg);
            textView.setTextColor(gpsInfoChild.m_color);

            CheckBox cb = m_colorsCb.get(gpsInfoChild.m_color);
            /*
                        if (cb != null && !cb.isChecked()) {
            ViewGroup.LayoutParams lp = expandView.getLayoutParams();
            lp.height = 0;
            expandView.setLayoutParams(lp);
            expandView.setVisibility(View.GONE);
                        }
            */
            return expandView;
        }

        @Override
        public int getGroupCount() {
            return m_list.size();
        }

        @Override
        public int getChildrenCount(int groupPosition) {
            return m_list.get(groupPosition).getCount();
        }

        @Override
        public Object getGroup(int groupPosition) {
            return null;
        }

        @Override
        public Object getChild(int groupPosition, int childPosition) {
            return null;
        }

        @Override
        public long getGroupId(int groupPosition) {
            return 0;
        }

        @Override
        public long getChildId(int groupPosition, int childPosition) {
            return 0;
        }

        @Override
        public boolean hasStableIds() {
            return true;
        }

        /**
         * Generate summary (row) presentation view object.
         */
        @Override
        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {

            View summaryView = convertView;
            if (null == summaryView) {
                summaryView = m_inflater.inflate(SUMMARY_LAYOUT, parent, false);
            }

            GpsInfo gpsInfo = m_list.get(groupPosition);
            if (gpsInfo != null) {
                GpsItem gpsItem = gpsInfo.getItem();

                long time = gpsItem.m_time;
                String timeStr = (time != s_noTime) ? s_hourFormat.format(time) + " " : "";
                String msg = timeStr + gpsItem.m_msg;

                TextView textView = Ui.viewById(summaryView, R.id.gpsColumn);
                textView.setText(msg);
                textView.setTextColor(gpsItem.m_color);
            }

            return summaryView;
        }

        @Override
        public boolean isChildSelectable(int groupPosition, int childPosition) {
            return true;
        }
    }
}