Move LocationTracker test app from vendor/google to frameworks/base/tests

Change-Id: I49f1bfe2081f2c48fcb22b74aa2377857c2bae6d
Signed-off-by: Mike Lockwood <lockwood@android.com>
diff --git a/tests/LocationTracker/Android.mk b/tests/LocationTracker/Android.mk
new file mode 100644
index 0000000..b142d22
--- /dev/null
+++ b/tests/LocationTracker/Android.mk
@@ -0,0 +1,10 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := LocationTracker
+
+LOCAL_MODULE_TAGS := tests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/LocationTracker/AndroidManifest.xml b/tests/LocationTracker/AndroidManifest.xml
new file mode 100644
index 0000000..9dbf4ef
--- /dev/null
+++ b/tests/LocationTracker/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.locationtracker">
+
+    <!-- Permissions for the Location Service -->
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+    <!--  Permission for wifi -->
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+
+    <!-- give the location tracker ability to induce device insomnia -->
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <!--  Permission for SD card -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application android:label="@string/app_label">
+        <activity android:name="TrackerActivity" android:label="Location Tracker">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <service android:name=".TrackerService" />
+        <activity android:label="@string/settings_menu" android:name="SettingsActivity" />
+        <provider android:name=".data.TrackerProvider"
+            android:authorities="com.google.android.locationtracker" />
+    </application>
+
+</manifest>
diff --git a/tests/LocationTracker/res/layout/entrylist_item.xml b/tests/LocationTracker/res/layout/entrylist_item.xml
new file mode 100644
index 0000000..8187677
--- /dev/null
+++ b/tests/LocationTracker/res/layout/entrylist_item.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2006-2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/entrylist_item"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:textAppearance="?android:attr/textAppearanceSmall"
+/>
diff --git a/tests/LocationTracker/res/menu/menu.xml b/tests/LocationTracker/res/menu/menu.xml
new file mode 100644
index 0000000..94c589a
--- /dev/null
+++ b/tests/LocationTracker/res/menu/menu.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+* Copyright (c) 2008 Google Inc.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*   http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/start_service_menu"
+          android:title="@string/start_service_menu" />
+    <item android:id="@+id/stop_service_menu"
+          android:title="@string/stop_service_menu" />
+    <item android:id="@+id/settings_menu"
+          android:title="@string/settings_menu" />
+    <item android:id="@+id/export_sub_menu"
+          android:title="@string/export_sub_menu">
+        <menu>
+            <item android:id="@+id/export_kml"
+                  android:title="@string/export_kml" />
+            <item android:id="@+id/export_csv"
+                  android:title="@string/export_csv" />
+        </menu>
+    </item>
+    <item android:title="@string/clear_data"
+          android:id="@+id/clear_data_menu"/>
+</menu>
diff --git a/tests/LocationTracker/res/values/strings.xml b/tests/LocationTracker/res/values/strings.xml
new file mode 100644
index 0000000..bb3ea86
--- /dev/null
+++ b/tests/LocationTracker/res/values/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2008 Google Inc.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+
+<resources>
+    <string name="start_service_menu">Start Service</string>
+    <string name="stop_service_menu">Stop Service</string>
+    <string name="settings_menu">Settings</string>
+    <string name="update_preference">Update frequency</string>
+    <string name="title_mintime_preference">Minimum update time</string>
+    <string name="summary_mintime_preference">The suggested minimum time interval for location updates, in seconds</string>
+    <string name="dialog_title_mintime_preference">Minimum update time</string>
+    <string name="title_mindistance_preference">Minimum distance</string>
+    <string name="summary_mindistance_preference">Minimum distance interval for location updates, in meters</string>
+    <string name="dialog_title_mindistance_preference">Minimum distance</string>
+    <string name="provider_preferences">Location providers</string>
+    <string name="title_network_preference">Network location</string>
+    <string name="summary_network_preference">Listen for updates to network location (Wi-Fi/cellid)</string>
+    <string name="title_gps_preference">GPS location</string>
+    <string name="summary_gps_preference">Listen for updates to GPS location</string>
+    <string name="title_signal_preference">Signal strength</string>
+    <string name="summary_signal_preference">Listen for updates to signal strength</string>
+    <string name="advanced_preferences">Advanced</string>
+    <string name="title_advanced_log_preference">Location debug logging</string>
+    <string name="summary_advanced_preference">Logs detailed location data, only relevant for location/test engineers</string>
+    <string name="app_label">Location Tracker</string>
+    <string name="export_sub_menu">Export\u2026</string>
+    <string name="export_kml">Export As KML</string>
+    <string name="export_csv">Export As CSV</string>
+    <string name="clear_data">Clear data</string>
+    <string name="delete_confirm">All current tracking data will be deleted.</string>
+    <string name="confirm_title">Clear data</string>
+</resources>
diff --git a/tests/LocationTracker/res/xml/preferences.xml b/tests/LocationTracker/res/xml/preferences.xml
new file mode 100755
index 0000000..b57837f
--- /dev/null
+++ b/tests/LocationTracker/res/xml/preferences.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 Google Inc.
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- The Location preferences UI -->
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <PreferenceCategory android:title="@string/update_preference">
+        <EditTextPreference android:key="mintime_preference"
+            android:defaultValue="0"
+            android:title="@string/title_mintime_preference"
+            android:summary="@string/summary_mintime_preference"
+            android:dialogTitle="@string/dialog_title_mintime_preference" />
+
+        <EditTextPreference android:key="mindistance_preference"
+            android:defaultValue="0"
+            android:title="@string/title_mindistance_preference"
+            android:summary="@string/summary_mindistance_preference"
+            android:dialogTitle="@string/dialog_title_mindistance_preference" />
+
+    </PreferenceCategory>
+
+    <PreferenceCategory android:title="@string/provider_preferences">
+
+        <CheckBoxPreference android:key="network_preference"
+            android:defaultValue="true"
+            android:title="@string/title_network_preference"
+            android:summary="@string/summary_network_preference" />
+
+        <CheckBoxPreference android:key="gps_preference"
+            android:defaultValue="true"
+            android:title="@string/title_gps_preference"
+            android:summary="@string/summary_gps_preference" />
+
+        <CheckBoxPreference android:key="signal_preference"
+            android:defaultValue="false"
+            android:title="@string/title_signal_preference"
+            android:summary="@string/summary_signal_preference" />
+
+    </PreferenceCategory>
+
+    <PreferenceCategory android:title="@string/advanced_preferences">
+
+        <CheckBoxPreference android:key="advanced_log_preference"
+            android:defaultValue="false"
+            android:title="@string/title_advanced_log_preference"
+            android:summary="@string/summary_advanced_preference" />
+    </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/SettingsActivity.java b/tests/LocationTracker/src/com/google/android/locationtracker/SettingsActivity.java
new file mode 100755
index 0000000..edfe65a
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/SettingsActivity.java
@@ -0,0 +1,35 @@
+/*

+ * Copyright (C) 2008 Google Inc.

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package com.google.android.locationtracker;

+

+import android.os.Bundle;

+import android.preference.PreferenceActivity;

+

+/**

+ * Settings preference screen for location tracker

+ */

+public class SettingsActivity extends PreferenceActivity {

+

+    @Override

+    protected void onCreate(Bundle savedInstanceState) {

+        super.onCreate(savedInstanceState);

+

+        // Load the preferences from an XML resource

+        addPreferencesFromResource(R.xml.preferences);

+    }

+

+}

diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/TrackerActivity.java b/tests/LocationTracker/src/com/google/android/locationtracker/TrackerActivity.java
new file mode 100644
index 0000000..a96c6bb
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/TrackerActivity.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.locationtracker;
+
+import com.google.android.locationtracker.data.DateUtils;
+import com.google.android.locationtracker.data.TrackerDataHelper;
+import com.google.android.locationtracker.data.TrackerListHelper;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.database.Cursor;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Activity for location tracker service
+ *
+ * Contains facilities for starting and
+ * stopping location tracker service, as well as displaying the current location
+ * data
+ *
+ * Future enhancements:
+ *   - export data as dB
+ *   - enable/disable "start service" and "stop service" menu items as
+ *     appropriate
+ */
+public class TrackerActivity extends ListActivity {
+
+    static final String LOG_TAG = "LocationTracker";
+
+    private TrackerListHelper mDataHelper;
+
+    /**
+     * Retrieves and displays the currently logged location data from file
+     *
+     * @param icicle
+     */
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        mDataHelper = new TrackerListHelper(this);
+        mDataHelper.bindListUI(R.layout.entrylist_item);
+    }
+
+    /**
+     * Builds the menu
+     *
+     * @param menu - menu to add items to
+     */
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater menuInflater = getMenuInflater();
+        menuInflater.inflate(R.menu.menu, menu);
+        return true;
+    }
+
+    /**
+     * Handles menu item selection
+     *
+     * @param item - the selected menu item
+     */
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.start_service_menu: {
+                startService(new Intent(TrackerActivity.this,
+                        TrackerService.class));
+                break;
+            }
+            case R.id.stop_service_menu: {
+                stopService(new Intent(TrackerActivity.this,
+                        TrackerService.class));
+                break;
+            }
+            case R.id.settings_menu: {
+                launchSettings();
+                break;
+            }
+            case R.id.export_kml: {
+                exportKML();
+                break;
+            }
+            case R.id.export_csv: {
+                exportCSV();
+                break;
+            }
+            case R.id.clear_data_menu: {
+                clearData();
+                break;
+            }
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void clearData() {
+        Runnable clearAction = new Runnable() {
+            public void run() {
+                TrackerDataHelper helper =
+                    new TrackerDataHelper(TrackerActivity.this);
+                helper.deleteAll();
+            }
+        };
+        showConfirm(R.string.delete_confirm, clearAction);
+    }
+
+    private void showConfirm(int textId, final Runnable onConfirmAction) {
+        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+        dialogBuilder.setTitle(R.string.confirm_title);
+        dialogBuilder.setMessage(textId);
+        dialogBuilder.setPositiveButton(android.R.string.ok,
+                new OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                onConfirmAction.run();
+            }
+        });
+        dialogBuilder.setNegativeButton(android.R.string.cancel,
+                new OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                // do nothing
+            }
+        });
+        dialogBuilder.show();
+    }
+
+    private void exportCSV() {
+        String exportFileName = getUniqueFileName("csv");
+        exportFile(null, exportFileName, new TrackerDataHelper(this,
+                TrackerDataHelper.CSV_FORMATTER));
+    }
+
+    private void exportKML() {
+        String exportFileName = getUniqueFileName(
+                LocationManager.NETWORK_PROVIDER + ".kml");
+        exportFile(LocationManager.NETWORK_PROVIDER, exportFileName,
+                new TrackerDataHelper(this, TrackerDataHelper.KML_FORMATTER));
+        exportFileName = getUniqueFileName(
+                LocationManager.GPS_PROVIDER + ".kml");
+        exportFile(LocationManager.GPS_PROVIDER, exportFileName,
+                new TrackerDataHelper(this, TrackerDataHelper.KML_FORMATTER));
+    }
+
+    private void exportFile(String tagFilter,
+                            String exportFileName,
+                            TrackerDataHelper trackerData) {
+        BufferedWriter exportWriter = null;
+        Cursor cursor = trackerData.query(tagFilter);
+        try {
+            exportWriter = new BufferedWriter(new FileWriter(exportFileName));
+            exportWriter.write(trackerData.getOutputHeader());
+
+            String line = null;
+
+            while ((line = trackerData.getNextOutput(cursor)) != null) {
+                exportWriter.write(line);
+            }
+            exportWriter.write(trackerData.getOutputFooter());
+            Toast.makeText(this, "Successfully exported data to " +
+                    exportFileName, Toast.LENGTH_SHORT).show();
+
+        } catch (IOException e) {
+            Toast.makeText(this, "Error exporting file: " +
+                    e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
+
+            Log.e(LOG_TAG, "Error exporting file", e);
+        } finally {
+            closeWriter(exportWriter);
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    private void closeWriter(Writer exportWriter) {
+        if (exportWriter != null) {
+            try {
+                exportWriter.close();
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "error closing file", e);
+            }
+        }
+    }
+
+    private String getUniqueFileName(String ext) {
+        File dir = new File("/sdcard/locationtracker");
+        if (!dir.exists()) {
+            dir.mkdir();
+        }
+        return "/sdcard/locationtracker/tracking-" +
+            DateUtils.getCurrentTimestamp() + "." + ext;
+    }
+
+    private void launchSettings() {
+        Intent settingsIntent = new Intent();
+        settingsIntent.setClass(this, SettingsActivity.class);
+        startActivity(settingsIntent);
+    }
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/TrackerService.java b/tests/LocationTracker/src/com/google/android/locationtracker/TrackerService.java
new file mode 100644
index 0000000..e7fdc494
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/TrackerService.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.locationtracker;
+
+import com.google.android.locationtracker.data.TrackerDataHelper;
+
+import android.app.Service;
+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.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.telephony.CellLocation;
+import android.telephony.PhoneStateListener;
+import android.telephony.SignalStrength;
+import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaCellLocation;
+import android.telephony.gsm.GsmCellLocation;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Location Tracking service
+ *
+ * Records location updates for all registered location providers, and cell
+ * location updates
+ */
+public class TrackerService extends Service {
+
+    private List<LocationTrackingListener> mListeners;
+
+    private static final String LOG_TAG = TrackerActivity.LOG_TAG;
+
+    // controls which location providers to track
+    private Set<String> mTrackedProviders;
+
+    private TrackerDataHelper mTrackerData;
+
+    private TelephonyManager mTelephonyManager;
+    private Location mNetworkLocation;
+
+    // Handlers and Receivers for phone and network state
+    private NetworkStateBroadcastReceiver mNetwork;
+    private static final String CELL_PROVIDER_TAG = "cell";
+    // signal strength updates
+    private static final String SIGNAL_PROVIDER_TAG = "signal";
+    private static final String WIFI_PROVIDER_TAG = "wifi";
+    // tracking tag for data connectivity issues
+    private static final String DATA_CONN_PROVIDER_TAG = "data";
+
+    // preference constants
+    private static final String MIN_TIME_PREF = "mintime_preference";
+    private static final String MIN_DIS_PREF = "mindistance_preference";
+    private static final String GPS_PREF = "gps_preference";
+    private static final String NETWORK_PREF = "network_preference";
+    private static final String SIGNAL_PREF = "signal_preference";
+    private static final String DEBUG_PREF = "advanced_log_preference";
+
+    private PreferenceListener mPrefListener;
+
+    public TrackerService() {
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        // ignore - nothing to do
+        return null;
+    }
+
+    /**
+     * registers location listeners
+     *
+     * @param intent
+     * @param startId
+     */
+    @Override
+    public void onStart(Intent intent, int startId) {
+        super.onStart(intent, startId);
+        mNetworkLocation = null;
+
+        initLocationListeners();
+        Toast.makeText(this, "Tracking service started", Toast.LENGTH_SHORT);
+    }
+
+    private synchronized void initLocationListeners() {
+        mTrackerData = new TrackerDataHelper(this);
+        LocationManager lm = getLocationManager();
+
+        mTrackedProviders = getTrackedProviders();
+
+        List<String> locationProviders = lm.getAllProviders();
+        mListeners = new ArrayList<LocationTrackingListener>(
+                locationProviders.size());
+
+        long minUpdateTime = getLocationUpdateTime();
+        float minDistance = getLocationMinDistance();
+
+        for (String providerName : locationProviders) {
+            if (mTrackedProviders.contains(providerName)) {
+                Log.d(LOG_TAG, "Adding location listener for provider " +
+                        providerName);
+                if (doDebugLogging()) {
+                    mTrackerData.writeEntry("init", String.format(
+                            "start listening to %s : %d ms; %f meters",
+                            providerName, minUpdateTime, minDistance));
+                }    
+                LocationTrackingListener listener =
+                    new LocationTrackingListener();
+                lm.requestLocationUpdates(providerName, minUpdateTime,
+                        minDistance, listener);
+                mListeners.add(listener);
+            }
+        }
+        mTelephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
+
+        if (doDebugLogging()) {
+            // register for cell location updates
+            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CELL_LOCATION);
+
+            // Register for Network (Wifi or Mobile) updates
+            mNetwork = new NetworkStateBroadcastReceiver();
+            IntentFilter mIntentFilter;
+            mIntentFilter = new IntentFilter();
+            mIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+            mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+            mIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+            Log.d(LOG_TAG, "registering receiver");
+            registerReceiver(mNetwork, mIntentFilter);
+        }
+
+        if (trackSignalStrength()) {
+            mTelephonyManager.listen(mPhoneStateListener,
+                    PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
+        }
+
+        // register for preference changes, so we can restart listeners on
+        // pref changes
+        mPrefListener = new PreferenceListener();
+        getPreferences().registerOnSharedPreferenceChangeListener(mPrefListener);
+    }
+
+    private Set<String> getTrackedProviders() {
+        Set<String> providerSet = new HashSet<String>();
+
+        if (trackGPS()) {
+            providerSet.add(LocationManager.GPS_PROVIDER);
+        }
+        if (trackNetwork()) {
+            providerSet.add(LocationManager.NETWORK_PROVIDER);
+        }
+        return providerSet;
+    }
+
+    private SharedPreferences getPreferences() {
+        return PreferenceManager.getDefaultSharedPreferences(this);
+    }
+
+    private boolean trackNetwork() {
+        return getPreferences().getBoolean(NETWORK_PREF, true);
+    }
+
+    private boolean trackGPS() {
+        return getPreferences().getBoolean(GPS_PREF, true);
+    }
+
+    private boolean doDebugLogging() {
+        return getPreferences().getBoolean(DEBUG_PREF, true);
+    }
+
+    private boolean trackSignalStrength() {
+        return getPreferences().getBoolean(SIGNAL_PREF, true);
+    }
+
+    private float getLocationMinDistance() {
+        try {
+            String disString = getPreferences().getString(MIN_DIS_PREF, "0");
+            return Float.parseFloat(disString);
+        }
+        catch (NumberFormatException e) {
+            Log.e(LOG_TAG, "Invalid preference for location min distance", e);
+        }
+        return 0;
+    }
+
+    private long getLocationUpdateTime() {
+        try {
+            String timeString = getPreferences().getString(MIN_TIME_PREF, "0");
+            long secondsTime = Long.valueOf(timeString);
+            return secondsTime * 1000;
+        }
+        catch (NumberFormatException e) {
+            Log.e(LOG_TAG, "Invalid preference for location min time", e);
+        }
+        return 0;
+    }
+
+    /**
+     * Shuts down this service
+     */
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        Log.d(LOG_TAG, "Removing location listeners");
+        stopListeners();
+        Toast.makeText(this, "Tracking service stopped", Toast.LENGTH_SHORT);
+    }
+
+    /**
+     * De-registers all location listeners, closes persistent storage
+     */
+    protected synchronized void stopListeners() {
+        LocationManager lm = getLocationManager();
+        if (mListeners != null) {
+            for (LocationTrackingListener listener : mListeners) {
+                lm.removeUpdates(listener);
+            }
+            mListeners.clear();
+        }
+        mListeners = null;
+
+        // stop cell state listener
+        if (mTelephonyManager != null) {
+            mTelephonyManager.listen(mPhoneStateListener, 0);
+        }    
+        
+        // stop network/wifi listener
+        if (mNetwork != null) {
+            unregisterReceiver(mNetwork);
+        }
+        mNetwork = null;
+
+        mTrackerData = null;
+        if (mPrefListener != null) {
+            getPreferences().unregisterOnSharedPreferenceChangeListener(mPrefListener);
+            mPrefListener = null;
+        }
+    }
+
+    private LocationManager getLocationManager() {
+        return (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+    }
+
+    /**
+     * Determine the current distance from given location to the last
+     * approximated network location
+     *
+     * @param location - new location
+     *
+     * @return float distance in meters
+     */
+    private synchronized float getDistanceFromNetwork(Location location) {
+        float value = 0;
+        if (mNetworkLocation != null) {
+            value = location.distanceTo(mNetworkLocation);
+        }
+        if (LocationManager.NETWORK_PROVIDER.equals(location.getProvider())) {
+            mNetworkLocation = location;
+        }
+        return value;
+    }
+
+    private class LocationTrackingListener implements LocationListener {
+
+        /**
+         * Writes details of location update to tracking file, including
+         * recording the distance between this location update and the last
+         * network location update
+         *
+         * @param location - new location
+         */
+        public void onLocationChanged(Location location) {
+            if (location == null) {
+                return;
+            }
+            float distance = getDistanceFromNetwork(location);
+            mTrackerData.writeEntry(location, distance);
+        }
+
+        /**
+         * Writes update to tracking file
+         *
+         * @param provider - name of disabled provider
+         */
+        public void onProviderDisabled(String provider) {
+            if (doDebugLogging()) {
+                mTrackerData.writeEntry(provider, "provider disabled");
+            }
+        }
+
+        /**
+         * Writes update to tracking file
+         * 
+         * @param provider - name of enabled provider
+         */
+        public void onProviderEnabled(String provider) {
+            if (doDebugLogging()) {
+                mTrackerData.writeEntry(provider,  "provider enabled");
+            }
+        }
+
+        /**
+         * Writes update to tracking file 
+         * 
+         * @param provider - name of provider whose status changed
+         * @param status - new status
+         * @param extras - optional set of extra status messages
+         */
+        public void onStatusChanged(String provider, int status, Bundle extras) {
+            if (doDebugLogging()) {
+                mTrackerData.writeEntry(provider,  "status change: " + status);
+            }
+        }
+    }
+
+    PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+        @Override
+        public void onCellLocationChanged(CellLocation location) {
+            try {
+                if (location instanceof GsmCellLocation) {
+                    GsmCellLocation cellLocation = (GsmCellLocation)location;
+                    String updateMsg = "cid=" + cellLocation.getCid() +
+                            ", lac=" + cellLocation.getLac();
+                    mTrackerData.writeEntry(CELL_PROVIDER_TAG, updateMsg);
+                } else if (location instanceof CdmaCellLocation) {
+                    CdmaCellLocation cellLocation = (CdmaCellLocation)location;
+                    String updateMsg = "BID=" + cellLocation.getBaseStationId() +
+                            ", SID=" + cellLocation.getSystemId() +
+                            ", NID=" + cellLocation.getNetworkId() +
+                            ", lat=" + cellLocation.getBaseStationLatitude() +
+                            ", long=" + cellLocation.getBaseStationLongitude() +
+                            ", SID=" + cellLocation.getSystemId() +
+                            ", NID=" + cellLocation.getNetworkId();
+                    mTrackerData.writeEntry(CELL_PROVIDER_TAG, updateMsg);
+                }
+            } catch (Exception e) {
+                Log.e(LOG_TAG, "Exception in CellStateHandler.handleMessage:", e);
+            }
+        }
+
+        public void onSignalStrengthsChanged(SignalStrength signalStrength) {
+            if (mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
+                String updateMsg = "cdma dBM=" + signalStrength.getCdmaDbm();
+                mTrackerData.writeEntry(SIGNAL_PROVIDER_TAG, updateMsg);
+            } else if  (mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
+                String updateMsg = "gsm signal=" + signalStrength.getGsmSignalStrength();
+                mTrackerData.writeEntry(SIGNAL_PROVIDER_TAG, updateMsg);
+            }
+        }
+    };
+
+    /**
+     * Listener + recorder for mobile or wifi updates
+     */
+    private class NetworkStateBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                WifiManager wifiManager =
+                    (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+                List<ScanResult> wifiScanResults = wifiManager.getScanResults();
+                String updateMsg = "num scan results=" +
+                    (wifiScanResults == null ? "0" : wifiScanResults.size());
+                mTrackerData.writeEntry(WIFI_PROVIDER_TAG, updateMsg);
+
+            } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+                String updateMsg;
+                boolean noConnectivity =
+                    intent.getBooleanExtra(
+                            ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+                if (noConnectivity) {
+                    updateMsg = "no connectivity";
+                }
+                else {
+                    updateMsg = "connection available";
+                }
+                mTrackerData.writeEntry(DATA_CONN_PROVIDER_TAG, updateMsg);
+
+            } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
+                int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
+                    WifiManager.WIFI_STATE_UNKNOWN);
+
+                String stateString = "unknown";
+                switch (state) {
+                    case WifiManager.WIFI_STATE_DISABLED:
+                        stateString = "disabled";
+                        break;
+                    case WifiManager.WIFI_STATE_DISABLING:
+                        stateString = "disabling";
+                        break;
+                    case WifiManager.WIFI_STATE_ENABLED:
+                        stateString = "enabled";
+                        break;
+                    case WifiManager.WIFI_STATE_ENABLING:
+                        stateString = "enabling";
+                        break;
+                }
+                mTrackerData.writeEntry(WIFI_PROVIDER_TAG,
+                        "state = " + stateString);
+            }
+        }
+    }
+
+    private class PreferenceListener implements OnSharedPreferenceChangeListener {
+
+        public void onSharedPreferenceChanged(
+                SharedPreferences sharedPreferences, String key) {
+            Log.d(LOG_TAG, "restarting listeners due to preference change");
+            synchronized (TrackerService.this) {
+                stopListeners();
+                initLocationListeners();
+            }
+        }
+    }
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/CSVFormatter.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/CSVFormatter.java
new file mode 100644
index 0000000..cd48db4
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/CSVFormatter.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+import com.google.android.locationtracker.data.TrackerEntry.EntryType;
+
+/**
+ * Formats tracker data as CSV output
+ */
+class CSVFormatter implements IFormatter {
+
+    private static final String DELIMITER = ", ";
+
+    public String getHeader() {
+        StringBuilder csvBuilder = new StringBuilder();
+        for (String col : TrackerEntry.ATTRIBUTES) {
+            // skip type and id column
+            if (!TrackerEntry.ENTRY_TYPE.equals(col) &&
+                !TrackerEntry.ID_COL.equals(col)) {
+                csvBuilder.append(col);
+                csvBuilder.append(DELIMITER);
+            }
+        }
+        csvBuilder.append("\n");
+        return csvBuilder.toString();
+    }
+
+    public String getOutput(TrackerEntry entry) {
+        StringBuilder rowOutput = new StringBuilder();
+        // these must match order of columns added in getHeader
+        rowOutput.append(entry.getTimestamp());
+        rowOutput.append(DELIMITER);
+        rowOutput.append(entry.getTag());
+        rowOutput.append(DELIMITER);
+        //rowOutput.append(entry.getType());
+        //rowOutput.append(DELIMITER);
+        if (entry.getType() == EntryType.LOCATION_TYPE) {
+            if (entry.getLocation().hasAccuracy()) {
+                rowOutput.append(entry.getLocation().getAccuracy());
+            }
+            rowOutput.append(DELIMITER);
+            rowOutput.append(entry.getLocation().getLatitude());
+            rowOutput.append(DELIMITER);
+            rowOutput.append(entry.getLocation().getLongitude());
+            rowOutput.append(DELIMITER);
+            if (entry.getLocation().hasAltitude()) {
+                rowOutput.append(entry.getLocation().getAltitude());
+            }
+            rowOutput.append(DELIMITER);
+            if (entry.getLocation().hasSpeed()) {
+                rowOutput.append(entry.getLocation().getSpeed());
+            }
+            rowOutput.append(DELIMITER);
+            if (entry.getLocation().hasBearing()) {
+                rowOutput.append(entry.getLocation().getBearing());
+            }
+            rowOutput.append(DELIMITER);
+            rowOutput.append(entry.getDistFromNetLocation());
+            rowOutput.append(DELIMITER);
+            rowOutput.append(DateUtils.getKMLTimestamp(entry.getLocation()
+                    .getTime()));
+            rowOutput.append(DELIMITER);
+        }
+        rowOutput.append(entry.getLogMsg());
+        rowOutput.append("\n");
+        return rowOutput.toString();
+    }
+
+    public String getFooter() {
+        // not needed, return empty string
+        return "";
+    }
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/DateUtils.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/DateUtils.java
new file mode 100644
index 0000000..78d6705
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/DateUtils.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+/**
+ * Provides formatting date as string utilities
+ */
+public class DateUtils {
+
+    private DateUtils() {
+
+    }
+
+    /**
+     * Returns timestamp given by param in KML format ie yyyy-mm-ddThh:mm:ssZ,
+     * where T is the separator between the date and the time and the time zone
+     * is Z (for UTC)
+     *
+     * @return KML timestamp as String
+     */
+    public static String getKMLTimestamp(long when) {
+        TimeZone tz = TimeZone.getTimeZone("GMT");
+        Calendar c = Calendar.getInstance(tz);
+        c.setTimeInMillis(when);
+        return String.format("%tY-%tm-%tdT%tH:%tM:%tSZ", c, c, c, c, c, c);
+    }
+
+    /**
+     * Helper version of getKMLTimestamp, that returns timestamp for current
+     * time
+     */
+    public static String getCurrentKMLTimestamp() {
+        return getKMLTimestamp(System.currentTimeMillis());
+    }
+
+    /**
+     * Returns timestamp in following format: yyyy-mm-dd-hh-mm-ss
+     */
+    public static String getCurrentTimestamp() {
+        Calendar c = Calendar.getInstance();
+        c.setTimeInMillis(System.currentTimeMillis());
+        return String.format("%tY-%tm-%td-%tH-%tM-%tS", c, c, c, c, c, c);
+    }
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/IFormatter.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/IFormatter.java
new file mode 100644
index 0000000..d921249
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/IFormatter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+/**
+ * interface for formatting tracker data output
+ */
+interface IFormatter {
+
+    String getHeader();
+    String getOutput(TrackerEntry entry);
+    String getFooter();
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/KMLFormatter.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/KMLFormatter.java
new file mode 100644
index 0000000..a49827c
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/KMLFormatter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+import com.google.android.locationtracker.data.TrackerEntry.EntryType;
+
+import android.location.Location;
+
+/**
+ * Formats tracker data as KML output
+ */
+class KMLFormatter implements IFormatter {
+
+    public String getHeader() {
+        LineBuilder builder = new LineBuilder();
+        builder.addLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+        builder.addLine("<kml xmlns=\"http://earth.google.com/kml/2.2\">");
+        builder.addLine("<Document>");
+        return builder.toString();
+    }
+
+    public String getFooter() {
+        LineBuilder builder = new LineBuilder();
+        builder.addLine("</Document>");
+        builder.addLine("</kml>");
+        return builder.toString();
+    }
+
+    public String getOutput(TrackerEntry entry) {
+        LineBuilder builder = new LineBuilder();
+
+        if (entry.getType() == EntryType.LOCATION_TYPE) {
+
+            Location loc = entry.getLocation();
+            builder.addLine("<Placemark>");
+            builder.addLine("<description>");
+            builder.addLine("accuracy = " + loc.getAccuracy());
+            builder.addLine("distance from last network location  = "
+                    + entry.getDistFromNetLocation());
+            builder.addLine("</description>");
+            builder.addLine("<TimeStamp>");
+            builder.addLine("<when>" + entry.getTimestamp() + "</when>");
+            builder.addLine("</TimeStamp>");
+            builder.addLine("<Point>");
+            builder.addLine("<coordinates>");
+            builder.addLine(loc.getLongitude() + "," + loc.getLatitude() + ","
+                    + loc.getAltitude());
+            builder.addLine("</coordinates>");
+            builder.addLine("</Point>");
+            builder.addLine("</Placemark>");
+        }
+        return builder.toString();
+    }
+
+    private static class LineBuilder {
+        private StringBuilder mBuilder;
+
+        public LineBuilder() {
+            mBuilder = new StringBuilder();
+        }
+
+        public void addLine(String line) {
+            mBuilder.append(line);
+            mBuilder.append("\n");
+        }
+
+        @Override
+        public String toString() {
+            return mBuilder.toString();
+        }
+
+    }
+
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerDataHelper.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerDataHelper.java
new file mode 100644
index 0000000..cdcc2b3
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerDataHelper.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.location.Location;
+
+/**
+ * Helper class for writing and retrieving data using the TrackerProvider
+ * content provider
+ *
+ */
+public class TrackerDataHelper {
+
+    private Context mContext;
+    /** formats data output */
+    protected IFormatter mFormatter;
+
+    /** formats output as Comma separated value CSV file */
+    public static final IFormatter CSV_FORMATTER = new CSVFormatter();
+    /** formats output as KML file */
+    public static final IFormatter KML_FORMATTER = new KMLFormatter();
+    /** provides no formatting */
+    public static final IFormatter NO_FORMATTER = new IFormatter() {
+        public String getFooter() {
+            return "";
+        }
+
+        public String getHeader() {
+            return "";
+        }
+
+        public String getOutput(TrackerEntry entry) {
+            return "";
+        }
+    };
+
+    /**
+     * Creates instance
+     *
+     * @param context - content context
+     * @param formatter - formats the output from the get*Output* methods
+     */
+    public TrackerDataHelper(Context context, IFormatter formatter) {
+        mContext = context;
+        mFormatter = formatter;
+    }
+
+    /**
+     * Creates a instance with no output formatting capabilities. Useful for
+     * clients that require write-only access
+     */
+    public TrackerDataHelper(Context context) {
+        this(context, NO_FORMATTER);
+    }
+
+    /**
+     * insert given TrackerEntry into content provider
+     */
+    void writeEntry(TrackerEntry entry) {
+        mContext.getContentResolver().insert(TrackerProvider.CONTENT_URI,
+                entry.getAsContentValues());
+    }
+
+    /**
+     * insert given location into tracker data
+     */
+    public void writeEntry(Location loc, float distFromNetLoc) {
+        writeEntry(TrackerEntry.createEntry(loc, distFromNetLoc));
+    }
+
+    /**
+     * insert given log message into tracker data
+     */
+    public void writeEntry(String tag, String logMsg) {
+        writeEntry(TrackerEntry.createEntry(tag, logMsg));
+    }
+
+    /**
+     * Deletes all tracker entries
+     */
+    public void deleteAll() {
+        mContext.getContentResolver().delete(TrackerProvider.CONTENT_URI, null,
+                null);
+    }
+
+    /**
+     * Query tracker data, filtering by given tag
+     *
+     * @param tag
+     * @return Cursor to data
+     */
+    public Cursor query(String tag, int limit) {
+        String selection = (tag == null ? null : TrackerEntry.TAG + "=?");
+        String[] selectionArgs = (tag == null ? null : new String[] {tag});
+        Cursor cursor = mContext.getContentResolver().query(
+                TrackerProvider.CONTENT_URI, TrackerEntry.ATTRIBUTES,
+                selection, selectionArgs, null);
+        if (cursor == null) {
+            return cursor;
+        }
+        int pos = (cursor.getCount() < limit ? 0 : cursor.getCount() - limit);
+        cursor.moveToPosition(pos);
+        return cursor;
+    }
+
+    /**
+     * Retrieves a cursor that starts at the last limit rows
+     *
+     * @param limit
+     * @return a cursor, null if bad things happened
+     */
+    public Cursor query(int limit) {
+        return query(null, limit);
+    }
+
+    /**
+     * Query tracker data, filtering by given tag. mo limit to number of rows
+     * returned
+     *
+     * @param tag
+     * @return Cursor to data
+     */
+    public Cursor query(String tag) {
+        return query(tag, Integer.MAX_VALUE);
+    }
+
+    /**
+     * Returns the output header particular to the associated formatter
+     */
+    public String getOutputHeader() {
+        return mFormatter.getHeader();
+    }
+
+    /**
+     * Returns the output footer particular to the associated formatter
+     */
+    public String getOutputFooter() {
+        return mFormatter.getFooter();
+    }
+
+    /**
+     * Helper method which converts row referenced by given cursor to a string
+     * output
+     *
+     * @param cursor
+     * @return CharSequence output, null if given cursor is invalid or no more
+     *         data
+     */
+    public String getNextOutput(Cursor cursor) {
+        if (cursor == null || cursor.isAfterLast()) {
+            return null;
+        }
+        String output = mFormatter.getOutput(TrackerEntry.createEntry(cursor));
+        cursor.moveToNext();
+        return output;
+    }
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerEntry.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerEntry.java
new file mode 100644
index 0000000..91e6dcc
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerEntry.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.location.Location;
+
+
+/**
+ * Class that holds a tracker entry. An entry can be either a valid location, or
+ * a simple log msg
+ *
+ * It provides a concrete data structure to represent data stored in the
+ * TrackerProvider
+ */
+class TrackerEntry {
+
+    static final String TIMESTAMP = "Timestamp";
+    static final String TAG = "Tag";
+    static final String ENTRY_TYPE = "Type";
+
+    private Location mLocation;
+    private float mDistFromNetLocation;
+    private String mLogMsg;
+
+    static final String ID_COL = "_id";
+    static final String ACCURACY = "Accuracy";
+    static final String LATITUDE = "Latitude";
+    static final String LONGITUDE = "Longitude";
+    static final String ALTITUDE = "Altitude";
+    static final String SPEED = "Speed";
+    static final String BEARING = "Bearing";
+    static final String DIST_NET_LOCATION = "DistFromNetLocation";
+    static final String LOC_TIME = "LocationTime";
+    static final String DEBUG_INFO = "DebugInfo";
+
+    static final String STRING_DATA = "STRING";
+    static final String INT_DATA = "INTEGER";
+    static final String REAL_DATA = "REAL";
+    static final String BLOB_DATA = "BLOB";
+
+    static final String[] ATTRIBUTES = {
+            ID_COL, TIMESTAMP, TAG, ENTRY_TYPE, ACCURACY, LATITUDE, LONGITUDE,
+            ALTITUDE, SPEED, BEARING, DIST_NET_LOCATION, LOC_TIME, DEBUG_INFO};
+    static final String[] ATTRIBUTES_DATA_TYPE = {
+            INT_DATA + " PRIMARY KEY", STRING_DATA, STRING_DATA, STRING_DATA,
+            REAL_DATA, REAL_DATA, REAL_DATA, REAL_DATA, REAL_DATA, REAL_DATA,
+            REAL_DATA, INT_DATA, STRING_DATA};
+
+    // location extra keys used to retrieve debug info
+    private static final String NETWORK_LOCATION_SOURCE_KEY =
+        "networkLocationSource";
+    private static final String NETWORK_LOCATION_TYPE_KEY =
+        "networkLocationType";
+    private static final String[] LOCATION_DEBUG_KEYS = {
+            NETWORK_LOCATION_SOURCE_KEY, NETWORK_LOCATION_TYPE_KEY};
+
+    enum EntryType {
+        LOCATION_TYPE, LOG_TYPE
+    }
+
+    private String mTimestamp;
+    private String mTag;
+    private EntryType mType;
+
+    private TrackerEntry(String tag, EntryType type) {
+        mType = type;
+        mTag = tag;
+        mLocation = null;
+    }
+
+    private TrackerEntry(Location loc) {
+        this(loc.getProvider(), EntryType.LOCATION_TYPE);
+        mLocation = new Location(loc);
+    }
+
+    /**
+     * Creates a TrackerEntry from a Location
+     */
+    static TrackerEntry createEntry(Location loc, float distFromNetLocation) {
+        TrackerEntry entry = new TrackerEntry(loc);
+
+        String timestampVal = DateUtils.getCurrentKMLTimestamp();
+        entry.setTimestamp(timestampVal);
+        entry.setDistFromNetLocation(distFromNetLocation);
+        return entry;
+    }
+
+    /**
+     * Creates a TrackerEntry from a log msg
+     */
+    static TrackerEntry createEntry(String tag, String msg) {
+        TrackerEntry entry = new TrackerEntry(tag, EntryType.LOG_TYPE);
+        String timestampVal = DateUtils.getCurrentKMLTimestamp();
+        entry.setTimestamp(timestampVal);
+        entry.setLogMsg(msg);
+        return entry;
+    }
+
+    private void setTimestamp(String timestamp) {
+        mTimestamp = timestamp;
+    }
+
+    EntryType getType() {
+        return mType;
+    }
+
+    private void setDistFromNetLocation(float distFromNetLocation) {
+        mDistFromNetLocation = distFromNetLocation;
+    }
+
+    private void setLogMsg(String msg) {
+        mLogMsg = msg;
+    }
+
+    private void setLocation(Location location) {
+        mLocation = location;
+    }
+
+    String getTimestamp() {
+        return mTimestamp;
+    }
+
+    String getTag() {
+        return mTag;
+    }
+
+    Location getLocation() {
+        return mLocation;
+    }
+
+    String getLogMsg() {
+        return mLogMsg;
+    }
+
+    float getDistFromNetLocation() {
+        return mDistFromNetLocation;
+    }
+
+    static void buildCreationString(StringBuilder builder) {
+        if (ATTRIBUTES.length != ATTRIBUTES_DATA_TYPE.length) {
+            throw new IllegalArgumentException(
+                    "Attribute length does not match data type length");
+        }
+        for (int i = 0; i < ATTRIBUTES_DATA_TYPE.length; i++) {
+            if (i != 0) {
+                builder.append(", ");
+            }
+            builder.append(String.format("%s %s", ATTRIBUTES[i],
+                    ATTRIBUTES_DATA_TYPE[i]));
+        }
+    }
+
+    ContentValues getAsContentValues() {
+        ContentValues cValues = new ContentValues(ATTRIBUTES.length);
+        cValues.put(TIMESTAMP, mTimestamp);
+        cValues.put(TAG, mTag);
+        cValues.put(ENTRY_TYPE, mType.toString());
+        if (mType == EntryType.LOCATION_TYPE) {
+            cValues.put(LATITUDE, mLocation.getLatitude());
+            cValues.put(LONGITUDE, mLocation.getLongitude());
+            if (mLocation.hasAccuracy()) {
+                cValues.put(ACCURACY, mLocation.getAccuracy());
+            }
+            if (mLocation.hasAltitude()) {
+                cValues.put(ALTITUDE, mLocation.getAltitude());
+            }
+            if (mLocation.hasSpeed()) {
+                cValues.put(SPEED, mLocation.getSpeed());
+            }
+            if (mLocation.hasBearing()) {
+                cValues.put(BEARING, mLocation.getBearing());
+            }
+            cValues.put(DIST_NET_LOCATION, mDistFromNetLocation);
+            cValues.put(LOC_TIME, mLocation.getTime());
+            StringBuilder debugBuilder = new StringBuilder("");
+            if (mLocation.getExtras() != null) {
+                for (String key : LOCATION_DEBUG_KEYS) {
+                    Object val = mLocation.getExtras().get(key);
+                    if (val != null) {
+                        debugBuilder.append(String.format("%s=%s; ", key, val
+                                .toString()));
+                    }
+                }
+            }
+            cValues.put(DEBUG_INFO, debugBuilder.toString());
+        } else {
+            cValues.put(DEBUG_INFO, mLogMsg);
+        }
+        return cValues;
+    }
+
+    static TrackerEntry createEntry(Cursor cursor) {
+        String timestamp = cursor.getString(cursor.getColumnIndex(TIMESTAMP));
+        String tag = cursor.getString(cursor.getColumnIndex(TAG));
+        String sType = cursor.getString(cursor.getColumnIndex(ENTRY_TYPE));
+        TrackerEntry entry = new TrackerEntry(tag, EntryType.valueOf(sType));
+        entry.setTimestamp(timestamp);
+        if (entry.getType() == EntryType.LOCATION_TYPE) {
+            Location location = new Location(tag);
+            location.setLatitude(cursor.getFloat(cursor
+                    .getColumnIndexOrThrow(LATITUDE)));
+            location.setLongitude(cursor.getFloat(cursor
+                    .getColumnIndexOrThrow(LONGITUDE)));
+
+            Float accuracy = getNullableFloat(cursor, ACCURACY);
+            if (accuracy != null) {
+                location.setAccuracy(accuracy);
+            }
+            Float altitude = getNullableFloat(cursor, ALTITUDE);
+            if (altitude != null) {
+                location.setAltitude(altitude);
+            }
+            Float bearing = getNullableFloat(cursor, BEARING);
+            if (bearing != null) {
+                location.setBearing(bearing);
+            }
+            Float speed = getNullableFloat(cursor, SPEED);
+            if (speed != null) {
+                location.setSpeed(speed);
+            }
+            location.setTime(cursor.getLong(cursor.getColumnIndex(LOC_TIME)));
+            entry.setLocation(location);
+        }
+        entry.setLogMsg(cursor.getString(cursor.getColumnIndex(DEBUG_INFO)));
+
+        return entry;
+    }
+
+    private static Float getNullableFloat(Cursor cursor, String colName) {
+        Float retValue = null;
+        int colIndex = cursor.getColumnIndexOrThrow(colName);
+        if (!cursor.isNull(colIndex)) {
+            retValue = cursor.getFloat(colIndex);
+        }
+        return retValue;
+    }
+}
diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerListHelper.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerListHelper.java
new file mode 100644
index 0000000..e292b45b
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerListHelper.java
@@ -0,0 +1,75 @@
+/*

+ * Copyright (C) 2008 Google Inc.

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not

+ * use this file except in compliance with the License. You may obtain a copy of

+ * the License at

+ *

+ * http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.android.locationtracker.data;

+

+import android.app.ListActivity;

+import android.content.Context;

+import android.database.Cursor;

+import android.view.View;

+import android.widget.ResourceCursorAdapter;

+import android.widget.TextView;

+

+import com.google.android.locationtracker.R;

+

+/**

+ * Used to bind Tracker data to a list view UI

+ */

+public class TrackerListHelper extends TrackerDataHelper {

+

+    private ListActivity mActivity;

+

+    // sort entries by most recent first

+    private static final String SORT_ORDER = TrackerEntry.ID_COL + " DESC";

+

+    public TrackerListHelper(ListActivity activity) {

+        super(activity, TrackerDataHelper.CSV_FORMATTER);

+        mActivity = activity;

+    }

+

+    /**

+     * Helper method for binding the list activities UI to the tracker data

+     * Tracker data will be sorted in most-recent first order

+     * Will enable automatic UI changes as tracker data changes

+     *

+     * @param layout - layout to populate data

+     */

+    public void bindListUI(int layout) {

+        Cursor cursor = mActivity.managedQuery(TrackerProvider.CONTENT_URI,

+                TrackerEntry.ATTRIBUTES, null, null, SORT_ORDER);

+        // Used to map tracker entries from the database to views

+        TrackerAdapter adapter = new TrackerAdapter(mActivity, layout, cursor);

+        mActivity.setListAdapter(adapter);

+        cursor.setNotificationUri(mActivity.getContentResolver(),

+                TrackerProvider.CONTENT_URI);

+

+    }

+

+    private class TrackerAdapter extends ResourceCursorAdapter {

+

+        public TrackerAdapter(Context context, int layout, Cursor c) {

+            super(context, layout, c);

+        }

+

+        @Override

+        public void bindView(View view, Context context, Cursor cursor) {

+            final TextView v = (TextView) view

+                    .findViewById(R.id.entrylist_item);

+            String rowText = mFormatter.getOutput(TrackerEntry

+                    .createEntry(cursor));

+            v.setText(rowText);

+        }

+    }

+}

diff --git a/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerProvider.java b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerProvider.java
new file mode 100644
index 0000000..052d813
--- /dev/null
+++ b/tests/LocationTracker/src/com/google/android/locationtracker/data/TrackerProvider.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.locationtracker.data;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * Content provider for location tracking.
+ *
+ * It is recommended to use the TrackerDataHelper class to access this data
+ * rather than this class directly
+ */
+public class TrackerProvider extends ContentProvider {
+
+    public static final Uri CONTENT_URI = Uri
+            .parse("content://com.google.android.locationtracker");
+
+    private static final String DB_NAME = "tracking.db";
+    private static final String TABLE_NAME = "tracking";
+    private static final int DB_VERSION = 1;
+
+    private static final String LOG_TAG = "TrackerProvider";
+
+    /**
+     * This class helps open, create, and upgrade the database file.
+     */
+    private static class DatabaseHelper extends SQLiteOpenHelper {
+
+        DatabaseHelper(Context context) {
+            super(context, DB_NAME, null, DB_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append(String.format("CREATE TABLE %s (", TABLE_NAME));
+            TrackerEntry.buildCreationString(queryBuilder);
+
+            queryBuilder.append(");");
+            db.execSQL(queryBuilder.toString());
+            db.setVersion(DB_VERSION);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // TODO: reimplement this when dB version changes
+            Log.w(LOG_TAG, "Upgrading database from version " + oldVersion
+                            + " to " + newVersion
+                            + ", which will destroy all old data");
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+            onCreate(db);
+        }
+    }
+
+    private DatabaseHelper mOpenHelper;
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new DatabaseHelper(getContext());
+        return true;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int result = db.delete(TABLE_NAME, selection, selectionArgs);
+        getContext().getContentResolver().notifyChange(uri, null);
+        return result;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        long rowId = db.insert(TABLE_NAME, null, values);
+        if (rowId > 0) {
+            Uri addedUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
+            getContext().getContentResolver().notifyChange(addedUri, null);
+            return addedUri;
+        }
+        return null;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        // TODO: extract limit from URI ?
+        Cursor cursor = db.query(TABLE_NAME, projection, selection,
+                selectionArgs, null, null, sortOrder);
+        getContext().getContentResolver().notifyChange(uri, null);
+        return cursor;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+}