diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 3f9c8689a..3e72ff098 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -1,6 +1,7 @@ 秒 + @@ -391,6 +392,14 @@ limitations under the License. + + + + + + + + 发送到... diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index 64afb48f1..43a843ebc 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -38,6 +38,7 @@ limitations under the License. shareUrlOnly signalSamplingFrequency splitFrequency + timestampTrackName autoResumeTrackCurrentRetry @@ -47,4 +48,4 @@ limitations under the License. backupToSd restoreFromSd - \ No newline at end of file + diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index cab226775..6212f9cf2 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -210,6 +210,10 @@ limitations under the License. Auto-resume track timeout Maximum time in minutes after which a previously recording track will not be resumed on phone reboot + Timestamp as track name + The default track name will be the date and time at the start of the track + Track names will have numbers which increment for each new track. + Pace Only supported in Android 2.0 or above diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 386240b6a..848bab5a2 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -91,7 +91,7 @@ limitations under the License. android:entries="@array/task_frequency_options" android:entryValues="@array/task_frequency_values" android:defaultValue="-1" /> - + + + + @@ -126,7 +134,7 @@ limitations under the License. android:title="@string/settings_backup_to_sd" android:summary="@string/settings_backup_to_sd_summary" android:persistent="false" /> - = 0; + } /* * Application lifetime events: @@ -260,6 +278,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, instance = this; providerUtils = MyTracksProviderUtils.Factory.get(this); menuManager = new MenuManager(this); + sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); // The volume we want to control is the Text-To-Speech volume int volumeStream = @@ -295,14 +314,18 @@ public class MyTracks extends TabActivity implements OnTouchListener, tabHost.addView(layout); layout.setOnTouchListener(this); - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (prefs != null) { + if (sharedPreferences != null) { selectedTrackId = - prefs.getLong(getString(R.string.selected_track_key), -1); - recordingTrackId = - prefs.getLong(getString(R.string.recording_track_key), -1); - prefs.registerOnSharedPreferenceChangeListener(this); + sharedPreferences.getLong(getString(R.string.selected_track_key), -1); + recordingTrackId = sharedPreferences.getLong( + getString(R.string.recording_track_key), -1); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); + Log.d(MyTracksConstants.TAG, "recordingTrackId: " + recordingTrackId + + ", selectedTrackId: " + selectedTrackId); + if (recordingTrackId > 0) { + Intent startIntent = new Intent(this, TrackRecordingService.class); + startService(startIntent); + } } // This will show the eula until the user accepts or quits the app. @@ -1008,20 +1031,15 @@ public class MyTracks extends TabActivity implements OnTouchListener, shareIntent.putExtra(Intent.EXTRA_SUBJECT, getResources().getText(R.string.share_map_subject).toString()); - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); boolean shareUrlOnly = true; - if (prefs != null) { - shareUrlOnly = - prefs.getBoolean(getString(R.string.share_url_only_key), false); + if (sharedPreferences != null) { + shareUrlOnly = sharedPreferences.getBoolean( + getString(R.string.share_url_only_key), false); } String url = MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId; - String msg = (shareUrlOnly - ? url - : String.format( - getResources().getText(R.string.share_map_body_format).toString(), - url)); + String msg = shareUrlOnly ? url : String.format( + getResources().getText(R.string.share_map_body_format).toString(), url); shareIntent.putExtra(Intent.EXTRA_TEXT, msg); startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.share_map).toString())); @@ -1235,9 +1253,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, private void startRecordingNewTrack() { try { recordingTrackId = trackRecordingService.startNewTrack(); + // Select the recording track. + setSelectedTrack(recordingTrackId); Toast.makeText(this, getString(R.string.status_now_recording), Toast.LENGTH_SHORT).show(); - setSelectedAndRecordingTrack(recordingTrackId, recordingTrackId); } catch (RemoteException e) { Toast.makeText(this, getString(R.string.error_unable_to_start_recording), @@ -1272,7 +1291,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, } catch (RemoteException e) { Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e); } - setRecordingTrack(-1); Intent intent = new Intent(MyTracks.this, MyTracksDetails.class); intent.putExtra("trackid", recordingTrackId); intent.putExtra("hasCancelButton", false); @@ -1327,9 +1345,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, public void setSelectedTrack(final long trackId) { runOnUiThread(new Runnable() { public void run() { - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - SharedPreferences.Editor editor = prefs.edit(); + SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putLong(getString(R.string.selected_track_key), trackId); editor.commit(); } @@ -1340,44 +1356,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, return selectedTrackId; } - /** - * Writes the recording track id to the shared preferences. - * Executed on the UI thread. - * - * @param trackId the id of the track - */ - private void setRecordingTrack(final long trackId) { - runOnUiThread(new Runnable() { - public void run() { - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(getString(R.string.recording_track_key), trackId); - editor.commit(); - } - }); - } - - /** - * Writes the selected and the recording track id to the shared preferences. - * Executed on UI thread. - */ - private void setSelectedAndRecordingTrack(final long theSelectedTrackId, - final long theRecordingTrackId) { - runOnUiThread(new Runnable() { - public void run() { - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (prefs != null) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(getString(R.string.selected_track_key), theSelectedTrackId); - editor.putLong(getString(R.string.recording_track_key), theRecordingTrackId); - editor.commit(); - } - } - }); - } - /** * Binds to track recording service if it is running. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index 32212e145..78d7645d4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -118,7 +118,7 @@ public class MyTracksMap extends MapActivity private double variation; /** - * From the shared preferences: + * From the shared preferences. */ private int minRequiredAccuracy = MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY; @@ -135,8 +135,8 @@ public class MyTracksMap extends MapActivity private Location currentLocation; /** - * A thread with a looper. Post to updateTrackHandler to execute Runnables on - * this thread. + * A thread with a looper. Post to updateTrackHandler to execute + * {@link Runnable}s on this thread. */ private HandlerThread updateTrackThread; @@ -146,6 +146,8 @@ public class MyTracksMap extends MapActivity private Handler updateTrackHandler; private MyTracksProviderUtils providerUtils; + + private SharedPreferences sharedPreferences; /** * A runnable that updates the track from the provider (looking for points @@ -331,12 +333,6 @@ public class MyTracksMap extends MapActivity @Override public void onChange(boolean selfChange) { Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange"); - // Check for any new locations and append them to the currently - // recording track: - if (!MyTracks.getInstance().isRecording()) { - // No track is being recorded. We should not be here. - return; - } if (!isRecordingSelected()) { // No track, or one other than the recording track is selected, // don't bother. @@ -361,30 +357,25 @@ public class MyTracksMap extends MapActivity } }; - // Read shared preferences and register change listener: - SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (preferences != null) { - minRequiredAccuracy = preferences.getInt( - getString(R.string.min_required_accuracy_key), - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - recordingTrackId = - preferences.getLong(getString(R.string.recording_track_key), -1); - long selectedTrackId = - preferences.getLong(getString(R.string.selected_track_key), -1); - if (selectedTrackId >= 0) { - setSelectedTrack(selectedTrackId); - } + // Read shared preferences and register change listener. + sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (sharedPreferences != null) { + reloadSharedPreferences(sharedPreferences, null); updateOptionsButton(); - preferences.registerOnSharedPreferenceChangeListener(this); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); } } @Override protected void onDestroy() { Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy"); + if (updateTrackThread != null) { - updateTrackThread.quit(); + ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread( + updateTrackThread); + } + if (sharedPreferences != null) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); } super.onDestroy(); @@ -451,6 +442,9 @@ public class MyTracksMap extends MapActivity Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume"); super.onResume(); + // Reload all preferences as they might have changed since last run. + reloadSharedPreferences(sharedPreferences, null); + // Make sure any updates that might have happened are propagated to the // Map overlay: observer.onChange(false); @@ -707,11 +701,14 @@ public class MyTracksMap extends MapActivity * @param trackId a given track id */ public void setSelectedTrack(final long trackId) { + Log.d(MyTracksConstants.TAG, "MyTracksMap.setSelectedTrack: " + + "selectedtTrackId = " + selectedTrackId + ", trackId = " + trackId); + if (selectedTrackId == trackId) { // Selected track did not change, nothing to do. mapOverlay.setTrackDrawingEnabled(isATrackSelected()); - mapView.invalidate(); updateOptionsButton(); + mapView.invalidate(); return; } @@ -786,7 +783,7 @@ public class MyTracksMap extends MapActivity menu.setHeaderTitle(R.string.tracklist_this_track); menu.add(0, MyTracksConstants.MENU_EDIT, 0, R.string.tracklist_edit_track); - if (!MyTracks.getInstance().isRecording() || !isRecordingSelected()) { + if (!isRecordingSelected()) { menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0, R.string.tracklist_send_to_google); SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0, @@ -906,30 +903,13 @@ public class MyTracksMap extends MapActivity @Override public void onSharedPreferenceChanged( final SharedPreferences sharedPreferences, final String key) { + Log.d(MyTracksConstants.TAG, + "MyTracksMap.onSharedPreferenceChanged: " + key); if (key != null) { uiHandler.post(new Runnable() { @Override public void run() { - if (key.equals(getString(R.string.min_required_accuracy_key))) { - minRequiredAccuracy = sharedPreferences.getInt( - getString(R.string.min_required_accuracy_key), - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - } else if (key.equals(getString(R.string.selected_track_key))) { - long selectedTrackId = - sharedPreferences.getLong( - getString(R.string.selected_track_key), - -1); - setSelectedTrack(selectedTrackId); - } else if (key.equals(getString(R.string.recording_track_key))) { - recordingTrackId = - sharedPreferences.getLong( - getString(R.string.recording_track_key), - -1); - if (isATrackSelected()) { - mapOverlay.setShowEndMarker(!isRecordingSelected()); - mapView.postInvalidate(); - } - } + reloadSharedPreferences(sharedPreferences, key); } }); } @@ -1009,6 +989,31 @@ public class MyTracksMap extends MapActivity } }; + private void reloadSharedPreferences(SharedPreferences sharedPreferences, + String key) { + if (key == null || + key.equals(getString(R.string.min_required_accuracy_key))) { + minRequiredAccuracy = sharedPreferences.getInt( + getString(R.string.min_required_accuracy_key), + MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); + } + if (key == null || key.equals(getString(R.string.recording_track_key))) { + recordingTrackId = sharedPreferences.getLong( + getString(R.string.recording_track_key), -1); + } + if (key == null || key.equals(getString(R.string.selected_track_key))) { + setSelectedTrack(sharedPreferences.getLong( + getString(R.string.selected_track_key), -1)); + } + + // Show end marker if the track has been selected and is not recording. + // Note: This check must be *after* a call to setSelectedTrack(...) above. + if (isATrackSelected()) { + mapOverlay.setShowEndMarker(!isRecordingSelected()); + mapView.postInvalidate(); + } + } + private void readAllNewTrackPoints() { int numPoints = mapOverlay.getNumLocations(); if (numPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index 403190a6c..9d68c9c27 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -52,7 +52,7 @@ import android.widget.Toast; public class StatsActivity extends Activity implements OnSharedPreferenceChangeListener { - private final StatsUtilities utils; + private StatsUtilities utils; private UIUpdateThread thread; private ContentObserver observer; @@ -94,7 +94,7 @@ public class StatsActivity extends Activity */ private boolean showCurrentSegment = false; - private final MyTracksProviderUtils providerUtils; + private MyTracksProviderUtils providerUtils; /** * A runnable for posting to the UI thread. Will update the total time field. @@ -133,16 +133,14 @@ public class StatsActivity extends Activity } } - public StatsActivity() { - utils = new StatsUtilities(this); - providerUtils = new MyTracksProviderUtilsImpl(this); - } - /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + utils = new StatsUtilities(this); + providerUtils = new MyTracksProviderUtilsImpl(getContentResolver()); + // The volume we want to control is the Text-To-Speech volume int volumeStream = new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index 1ea043aff..56c70db13 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -345,7 +345,7 @@ public interface MyTracksProviderUtils { * Creates an instance of {@link MyTracksProviderUtils}. */ protected MyTracksProviderUtils newForContext(Context context) { - return new MyTracksProviderUtilsImpl(context); + return new MyTracksProviderUtilsImpl(context.getContentResolver()); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index ffbd77d9c..1e814f102 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -17,8 +17,8 @@ package com.google.android.apps.mytracks.content; import com.google.android.apps.mytracks.stats.TripStatistics; +import android.content.ContentResolver; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; import android.location.Location; import android.net.Uri; @@ -35,10 +35,10 @@ import java.util.List; */ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { - private final Context context; + private final ContentResolver contentResolver; - public MyTracksProviderUtilsImpl(Context context) { - this.context = context; + public MyTracksProviderUtilsImpl(ContentResolver contentResolver) { + this.contentResolver = contentResolver; } /** @@ -448,25 +448,22 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public void deleteAllTracks() { - context.getContentResolver().delete(TracksColumns.CONTENT_URI, null, null); - context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI, - null, null); - context.getContentResolver().delete( - WaypointsColumns.CONTENT_URI, null, null); + contentResolver.delete(TracksColumns.CONTENT_URI, null, null); + contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null); + contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null); } @Override public void deleteTrack(long trackId) { Track track = getTrack(trackId); if (track != null) { - context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI, + contentResolver.delete(TrackPointsColumns.CONTENT_URI, "_id>=" + track.getStartId() + " AND _id<=" + track.getStopId(), null); } - context.getContentResolver().delete(WaypointsColumns.CONTENT_URI, + contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=" + trackId, null); - context.getContentResolver().delete( - TracksColumns.CONTENT_URI, "_id=" + trackId, null); + contentResolver.delete(TracksColumns.CONTENT_URI, "_id=" + trackId, null); } @Override @@ -491,7 +488,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { "No statistics marker after the deleted one was found."); } } - context.getContentResolver().delete( + contentResolver.delete( WaypointsColumns.CONTENT_URI, "_id=" + waypointId, null); } @@ -503,7 +500,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { final String sortOrder = WaypointsColumns._ID + " LIMIT 1"; Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, selection, @@ -525,7 +522,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public boolean updateWaypoint(Waypoint waypoint) { try { - final int rows = context.getContentResolver().update( + final int rows = contentResolver.update( WaypointsColumns.CONTENT_URI, createContentValues(waypoint), "_id=" + waypoint.getId(), @@ -546,7 +543,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private Location findLocationBy(String select) { Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TrackPointsColumns.CONTENT_URI, null, select, null, null); if (cursor != null && cursor.moveToNext()) { return createLocation(cursor); @@ -570,7 +567,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private Track findTrackBy(String select) { Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TracksColumns.CONTENT_URI, null, select, null, null); if (cursor != null && cursor.moveToNext()) { return createTrack(cursor); @@ -592,7 +589,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Waypoint getFirstWaypoint(long trackId) { - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, "trackid=" + trackId, @@ -614,7 +611,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Waypoint getWaypoint(long waypointId) { - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, "_id=" + waypointId, @@ -637,7 +634,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastLocationId(long trackId) { final String[] projection = {"_id"}; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( TrackPointsColumns.CONTENT_URI, projection, "_id=(select max(_id) from trackpoints WHERE trackid=" + trackId + ")", @@ -661,7 +658,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getFirstWaypointId(long trackId) { final String[] projection = {"_id"}; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, projection, "trackid=" + trackId, @@ -685,7 +682,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastWaypointId(long trackId) { final String[] projection = {"_id"}; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, projection, WaypointsColumns.TRACKID + "=" + trackId, @@ -710,7 +707,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { public Track getLastTrack() { Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TracksColumns.CONTENT_URI, null, "_id=(select max(_id) from tracks)", null, null); if (cursor != null && cursor.moveToNext()) { @@ -729,7 +726,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastTrackId() { String[] proj = { TracksColumns._ID }; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( TracksColumns.CONTENT_URI, proj, "_id=(select max(_id) from tracks)", null, null); if (cursor != null) { @@ -769,7 +766,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { sortOrder += " LIMIT " + maxLocations; } - return context.getContentResolver().query( + return contentResolver.query( TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder); } @@ -791,7 +788,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { sortOrder += " LIMIT " + maxWaypoints; } - return context.getContentResolver().query( + return contentResolver.query( WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder); } @@ -847,7 +844,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Cursor getTracksCursor(String selection) { - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( TracksColumns.CONTENT_URI, null, selection, null, "_id"); return cursor; } @@ -855,14 +852,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Uri insertTrack(Track track) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrack"); - return context.getContentResolver().insert(TracksColumns.CONTENT_URI, + return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track)); } @Override public Uri insertTrackPoint(Location location, long trackId) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrackPoint"); - return context.getContentResolver().insert(TrackPointsColumns.CONTENT_URI, + return contentResolver.insert(TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId)); } @@ -875,14 +872,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { values[i] = createContentValues(locations[i], trackId); } - return context.getContentResolver().bulkInsert(TrackPointsColumns.CONTENT_URI, values); + return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values); } @Override public Uri insertWaypoint(Waypoint waypoint) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint"); waypoint.setId(-1); - return context.getContentResolver().insert(WaypointsColumns.CONTENT_URI, + return contentResolver.insert(WaypointsColumns.CONTENT_URI, createContentValues(waypoint)); } @@ -891,7 +888,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { Cursor cursor = null; try { final String[] projection = { TracksColumns._ID }; - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TracksColumns.CONTENT_URI, projection, TracksColumns._ID + "=" + id/*selection*/, @@ -911,7 +908,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public void updateTrack(Track track) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.updateTrack"); - context.getContentResolver().update(TracksColumns.CONTENT_URI, + contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), "_id=" + track.getId(), null); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java index e89b44e01..571859cc9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -468,8 +468,7 @@ public class GpxImporter extends DefaultHandler { // initialize start time with time of first track point if (statsBuilder == null) { - statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(time); + statsBuilder = new TripStatisticsBuilder(time); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java new file mode 100644 index 000000000..802199460 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2010 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.apps.mytracks.services; + +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.format.Time; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Creates a default track name based on the current default track name policy. + * + * @author Matthew Simmons + */ +class DefaultTrackNameFactory { + private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm"; + + private final Context context; + + DefaultTrackNameFactory(Context context) { + this.context = context; + } + + /** + * Creates a new track name. + * + * @param trackId The ID for the current track. + * @param startTime The start time, in milliseconds since the epoch, of the + * current track. + * @return The new track name. + */ + String newTrackName(long trackId, long startTime) { + if (useTimestampTrackName()) { + SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT); + return formatter.format(new Date(startTime)); + } else { + return String.format(context.getString(R.string.new_track), trackId); + } + } + + /** Determines whether the preferences allow a timestamp-based track name */ + protected boolean useTimestampTrackName() { + SharedPreferences prefs = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + return prefs.getBoolean( + context.getString(R.string.timestamp_track_name_key), true); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index 77e7e7f04..311c17526 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.MyTracksSettings; import com.google.android.maps.mytracks.R; import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.util.Log; /** @@ -30,6 +31,7 @@ import android.util.Log; public class PreferenceManager { private TrackRecordingService service; private final String announcementFrequencyKey; + private final String autoResumeTrackCurrentRetryKey; private final String autoResumeTrackTimeoutKey; private final String maxRecordingDistanceKey; private final String metricUnitsKey; @@ -40,11 +42,22 @@ public class PreferenceManager { private final String signalSamplingFrequencyKey; private final String splitFrequencyKey; + private final SharedPreferences sharedPreferences; + public PreferenceManager(TrackRecordingService service) { this.service = service; - + this.sharedPreferences = + service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (sharedPreferences == null) { + Log.w(MyTracksConstants.TAG, + "TrackRecordingService: Couldn't get shared preferences."); + throw new IllegalStateException("Couldn't get shared preferences"); + } + announcementFrequencyKey = service.getString(R.string.announcement_frequency_key); + autoResumeTrackCurrentRetryKey = + service.getString(R.string.auto_resume_track_current_retry_key); autoResumeTrackTimeoutKey = service.getString(R.string.auto_resume_track_timeout_key); maxRecordingDistanceKey = @@ -57,12 +70,12 @@ public class PreferenceManager { service.getString(R.string.min_recording_interval_key); minRequiredAccuracyKey = service.getString(R.string.min_required_accuracy_key); - splitFrequencyKey = - service.getString(R.string.split_frequency_key); - signalSamplingFrequencyKey = - service.getString(R.string.signal_sampling_frequency_key); recordingTrackKey = service.getString(R.string.recording_track_key); + signalSamplingFrequencyKey = + service.getString(R.string.signal_sampling_frequency_key); + splitFrequencyKey = + service.getString(R.string.split_frequency_key); } /** @@ -72,14 +85,6 @@ public class PreferenceManager { * @param key the key that changed (may be null to update all preferences) */ public void onSharedPreferenceChanged(String key) { - SharedPreferences sharedPreferences = - service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (sharedPreferences == null) { - Log.w(MyTracksConstants.TAG, - "TrackRecordingService: Couldn't get shared preferences."); - return; - } - if (key == null || key.equals(minRecordingDistanceKey)) { service.setMinRecordingDistance( sharedPreferences.getInt( @@ -156,4 +161,16 @@ public class PreferenceManager { sharedPreferences.getBoolean(metricUnitsKey, true)); } } + + public void setAutoResumeTrackCurrentRetry(int retryAttempts) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts); + editor.commit(); + } + + public void setRecordingTrack(long id) { + Editor editor = sharedPreferences.edit(); + editor.putLong(recordingTrackKey, id); + editor.commit(); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 1ed1a7b4f..9b2b667ae 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -28,6 +28,7 @@ import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiPlatformAdapter; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; @@ -51,8 +52,10 @@ import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; +import android.text.format.DateFormat; import android.util.Log; +import java.util.Date; import java.util.Timer; import java.util.TimerTask; @@ -67,7 +70,7 @@ public class TrackRecordingService extends Service implements LocationListener { private static final String STATISTICS_ICON_URL = "http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png"; - private static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; + static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; private NotificationManager notificationManager; private LocationManager locationManager; @@ -94,8 +97,8 @@ public class TrackRecordingService extends Service implements LocationListener { */ private MyTracksProviderUtils providerUtils; - private TripStatisticsBuilder statsBuilder = new TripStatisticsBuilder(); - private TripStatisticsBuilder waypointStatsBuilder = new TripStatisticsBuilder(); + private TripStatisticsBuilder statsBuilder; + private TripStatisticsBuilder waypointStatsBuilder; /** * Current length of the recorded track. This length is calculated from the @@ -329,6 +332,8 @@ public class TrackRecordingService extends Service implements LocationListener { * Shows the notification message and icon in the notification bar. */ public void showNotification() { + final ApiPlatformAdapter apiPlatformAdapter = + ApiFeatures.getInstance().getApiPlatformAdapter(); if (isRecording) { Notification notification = new Notification( R.drawable.arrow_320, null /* tickerText */, @@ -339,9 +344,10 @@ public class TrackRecordingService extends Service implements LocationListener { notification.setLatestEventInfo(this, getString(R.string.app_name), getString(R.string.recording_your_track), contentIntent); notification.flags += Notification.FLAG_NO_CLEAR; - notificationManager.notify(1, notification); + apiPlatformAdapter.startForeground(this, notificationManager, 1, + notification); } else { - notificationManager.cancelAll(); + apiPlatformAdapter.stopForeground(this, notificationManager, 1); } } @@ -390,9 +396,11 @@ public class TrackRecordingService extends Service implements LocationListener { } private void restoreStats(Track track) { + Log.d(MyTracksConstants.TAG, + "Restoring stats of track with ID: " + track.getId()); + TripStatistics stats = track.getStatistics(); - statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(stats.getStartTime()); + statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); setUpAnnouncer(); signalManager.restore(); @@ -403,11 +411,11 @@ public class TrackRecordingService extends Service implements LocationListener { Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId); if (waypoint != null) { currentWaypointId = waypoint.getId(); - waypointStatsBuilder = new TripStatisticsBuilder(waypoint.getStatistics()); + waypointStatsBuilder = new TripStatisticsBuilder( + waypoint.getStatistics()); } else { // This should never happen, but we got to do something so life goes on: - waypointStatsBuilder = new TripStatisticsBuilder(); - waypointStatsBuilder.resumeAt(stats.getStartTime()); + waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime()); currentWaypointId = -1; } @@ -431,7 +439,7 @@ public class TrackRecordingService extends Service implements LocationListener { } statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); statsBuilder.pauseAt(stats.getStopTime()); - statsBuilder.resume(); + statsBuilder.resumeAt(System.currentTimeMillis()); } else { Log.e(MyTracksConstants.TAG, "Could not get track points cursor."); } @@ -656,6 +664,11 @@ public class TrackRecordingService extends Service implements LocationListener { if (recordingTrack != null) { restoreStats(recordingTrack); isRecording = true; + } else { + // Make sure we have consistent state in shared preferences. + Log.w(MyTracksConstants.TAG, "TrackRecordingService.onCreate: Resetting " + + "an orphaned recording track: " + recordingTrackId); + prefManager.setRecordingTrack(recordingTrackId = -1); } showNotification(); } @@ -739,8 +752,10 @@ public class TrackRecordingService extends Service implements LocationListener { // Make sure that the current track exists and is fresh enough. if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) { Log.i(MyTracksConstants.TAG, - "TrackRecordingService: Not resuming because the previous track " - + "doesn't exist or is too old"); + "TrackRecordingService: Not resuming, because the previous track (" + + recordingTrack + ") doesn't exist or is too old"); + isRecording = false; + prefManager.setRecordingTrack(recordingTrackId = -1); stopSelfResult(startId); return; } @@ -753,16 +768,12 @@ public class TrackRecordingService extends Service implements LocationListener { SharedPreferences sharedPreferences, int retryAttempts) { Log.d(MyTracksConstants.TAG, "Updating auto-resume retry attempts to: " + retryAttempts); - - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putInt( - getString(R.string.auto_resume_track_current_retry_key), retryAttempts); - editor.commit(); + prefManager.setAutoResumeTrackCurrentRetry(retryAttempts); } private boolean shouldResumeTrack(Track track) { - Log.d(MyTracksConstants.TAG, - "maybeResumeTrack: autoResumeTrackTimeout = " + autoResumeTrackTimeout); + Log.d(MyTracksConstants.TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + + autoResumeTrackTimeout); // Check if we haven't exceeded the maximum number of retry attempts. SharedPreferences sharedPreferences = @@ -770,11 +781,11 @@ public class TrackRecordingService extends Service implements LocationListener { int retries = sharedPreferences.getInt( getString(R.string.auto_resume_track_current_retry_key), 0); Log.d(MyTracksConstants.TAG, - "TrackRecordingService: Attempting to auto-resume the track (" + "shouldResumeTrack: Attempting to auto-resume the track (" + (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")"); if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { Log.i(MyTracksConstants.TAG, - "TrackRecordingService: Not resuming because exceeded the maximum " + "shouldResumeTrack: Not resuming because exceeded the maximum " + "number of auto-resume retries"); return false; } @@ -785,9 +796,13 @@ public class TrackRecordingService extends Service implements LocationListener { // Check for special cases. if (autoResumeTrackTimeout == 0) { // Never resume. + Log.d(MyTracksConstants.TAG, + "shouldResumeTrack: Auto-resume disabled (never resume)"); return false; } else if (autoResumeTrackTimeout == -1) { // Always resume. + Log.d(MyTracksConstants.TAG, + "shouldResumeTrack: Auto-resume forced (always resume)"); return true; } @@ -795,7 +810,8 @@ public class TrackRecordingService extends Service implements LocationListener { long lastModified = track.getStatistics() != null ? track.getStatistics().getStopTime() : 0; Log.d(MyTracksConstants.TAG, - "maybeResumeTrack: lastModified = " + lastModified); + "shouldResumeTrack: lastModified = " + lastModified + + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); return lastModified > 0 && System.currentTimeMillis() - lastModified <= autoResumeTrackTimeout * 60 * 1000; } @@ -805,6 +821,11 @@ public class TrackRecordingService extends Service implements LocationListener { } public long insertWaypointMarker(Waypoint waypoint) { + if (!isRecording()) { + throw new IllegalStateException( + "Unable to insert waypoint marker while not recording!"); + } + if (waypoint.getLocation() != null) { waypoint.setLength(length); waypoint.setDuration(waypoint.getLocation().getTime() @@ -823,6 +844,11 @@ public class TrackRecordingService extends Service implements LocationListener { * @return the unique id of the inserted marker */ public long insertStatisticsMarker(Location location) { + if (!isRecording()) { + throw new IllegalStateException( + "Unable to insert statistics marker while not recording!"); + } + StringUtils utils = new StringUtils(TrackRecordingService.this); // Create a new waypoint to save @@ -850,8 +876,7 @@ public class TrackRecordingService extends Service implements LocationListener { Uri uri = providerUtils.insertWaypoint(waypoint); // Create a new stats keeper for the next marker - waypointStatsBuilder = new TripStatisticsBuilder(); - waypointStatsBuilder.resumeAt(time); + waypointStatsBuilder = new TripStatisticsBuilder(time); updateCurrentWaypoint(); return Long.parseLong(uri.getLastPathSegment()); } @@ -908,6 +933,10 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public void endCurrentTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack"); + if (recordingTrackId == -1 || !isRecording) { + throw new IllegalStateException("No recording track in progress!"); + } + isRecording = false; Track recordingTrack = providerUtils.getTrack(recordingTrackId); if (recordingTrack != null) { @@ -927,12 +956,15 @@ public class TrackRecordingService extends Service implements LocationListener { "_id=" + recordingTrack.getId(), null); } showNotification(); - recordingTrackId = -1; + prefManager.setRecordingTrack(recordingTrackId = -1); } @Override public void deleteAllTracks() { - endCurrentTrack(); + if (isRecording()) { + throw new IllegalStateException( + "Cannot delete all tracks while recording!"); + } providerUtils.deleteAllTracks(); } @@ -951,32 +983,41 @@ public class TrackRecordingService extends Service implements LocationListener { public long startNewTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack"); + if (recordingTrackId != -1 || isRecording) { + throw new IllegalStateException("A track is already in progress!"); + } + + long startTime = System.currentTimeMillis(); + Track track = new Track(); TripStatistics trackStats = track.getStatistics(); - track.setName("new"); - long startTime = System.currentTimeMillis(); trackStats.setStartTime(startTime); track.setStartId(-1); Uri trackUri = providerUtils.insertTrack(track); recordingTrackId = Long.parseLong(trackUri.getLastPathSegment()); track.setId(recordingTrackId); - track.setName(String.format(getString(R.string.new_track), recordingTrackId)); - providerUtils.updateTrack(track); - currentWaypointId = insertStatisticsMarker(null); + track.setName(new DefaultTrackNameFactory(this).newTrackName( + recordingTrackId, startTime)); isRecording = true; isMoving = true; - statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(startTime); + + providerUtils.updateTrack(track); + statsBuilder = new TripStatisticsBuilder(startTime); + waypointStatsBuilder = new TripStatisticsBuilder(startTime); + currentWaypointId = insertStatisticsMarker(null); setUpAnnouncer(); length = 0; showNotification(); registerLocationListener(); splitManager.restore(); signalManager.restore(); + // Reset the number of auto-resume retries. - SharedPreferences sharedPreferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - setAutoResumeTrackRetries(sharedPreferences, 0); + setAutoResumeTrackRetries( + getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0); + // Persist the current recording track. + prefManager.setRecordingTrack(recordingTrackId); + return recordingTrackId; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index f0107e832..8882ae7d2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -36,13 +36,13 @@ public class TripStatisticsBuilder { /** * The last location that the gps reported. */ - private Location lastLocation = null; + private Location lastLocation; /** * The last location that contributed to the stats. It is also the last * location the user was found to be moving. */ - private Location lastMovingLocation = null; + private Location lastMovingLocation; /** * The current speed in meters/second as reported by the gps. @@ -90,10 +90,13 @@ public class TripStatisticsBuilder { private long totalLocations = 0; /** - * Creates a new trip starting at the current system time. + * Creates a new trip starting at the given time. + * + * @param startTime the start time. */ - public TripStatisticsBuilder() { + public TripStatisticsBuilder(long startTime) { data = new TripStatistics(); + resumeAt(startTime); } /** @@ -103,6 +106,9 @@ public class TripStatisticsBuilder { */ public TripStatisticsBuilder(TripStatistics statsData) { data = new TripStatistics(statsData); + if (data.getStartTime() > 0) { + resumeAt(data.getStartTime()); + } } /** @@ -140,8 +146,8 @@ public class TripStatisticsBuilder { // Don't do anything if we didn't move since last fix: double distance = lastLocation.distanceTo(currentLocation); - if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE - && currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) { + if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE && + currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) { lastLocation = currentLocation; return false; } @@ -291,13 +297,6 @@ public class TripStatisticsBuilder { data.updateGradeExtremities(gradeBuffer.getAverage()); } - /** - * Pauses the track at the current time. - */ - public void pause() { - pauseAt(System.currentTimeMillis()); - } - /** * Pauses the track at the given time. * @@ -312,13 +311,6 @@ public class TripStatisticsBuilder { paused = true; } - /** - * Resumes the current track at the current time. - */ - public void resume() { - resumeAt(System.currentTimeMillis()); - } - /** * Resumes the current track at the given time. * @@ -364,8 +356,7 @@ public class TripStatisticsBuilder { } public TripStatistics getStatistics() { - // Take a snapshot - we do't want anyone messing with our internals + // Take a snapshot - we don't want anyone messing with our internals return new TripStatistics(data); } } - diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java index b6a4e02fd..7979e12b5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java @@ -15,7 +15,10 @@ */ package com.google.android.apps.mytracks.util; +import com.google.android.apps.mytracks.MyTracksConstants; + import android.os.Build; +import android.util.Log; /** * Utility class for determining if newer-API features are available on the @@ -28,9 +31,15 @@ public class ApiFeatures { /** * The API level of the Android version we're being run under. */ - public static final int ANDROID_API_LEVEL = Integer.parseInt(Build.VERSION.SDK); - + public static final int ANDROID_API_LEVEL = Integer.parseInt( + Build.VERSION.SDK); + private static ApiFeatures instance; + + /** + * The API platform adapter supported by this system. + */ + private ApiPlatformAdapter apiPlatformAdapter; /** * Returns the singleton instance of this class. @@ -52,7 +61,28 @@ public class ApiFeatures { /** * Allow subclasses for mocking, but no direct instantiation. */ - protected ApiFeatures() {} + protected ApiFeatures() { + if (getApiLevel() >= 5) { + try { + Class clazz = Class.forName( + "com.google.android.apps.mytracks.util.EclairPlatformAdapter"); + apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance(); + } catch (Exception e) { + Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair" + + " platform adapter", e); + } + } + if (apiPlatformAdapter == null) { + Log.i(MyTracksConstants.TAG, + "ApiFeatures: Using default platform adapter"); + // Cupcake adapter is always supported, so it's safe to do static linkage. + apiPlatformAdapter = new CupcakePlatformAdapter(); + } + } + + public ApiPlatformAdapter getApiPlatformAdapter() { + return apiPlatformAdapter; + } /** * Returns whether cloud backup (a.k.a. Froyo backup) is available. @@ -81,7 +111,7 @@ public class ApiFeatures { public boolean hasModernSignalStrength() { return getApiLevel() >= 7; } - + // Visible for testing. protected int getApiLevel() { return ANDROID_API_LEVEL; diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java new file mode 100644 index 000000000..1e2a5210a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010 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.apps.mytracks.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.os.HandlerThread; + +/** + * A set of methods that may be implemented in a platform specific way. + * + * @author Bartlomiej Niechwiej + */ +public interface ApiPlatformAdapter { + + /** + * Puts the specified service into foreground. + * + * @param service the service to be put in foreground. + * @param notificationManager the notification manager used to post the given + * notification. + * @param id the ID of the notification, unique within the application. + * @param notification the notification to post. + */ + void startForeground(Service service, NotificationManager notificationManager, + int id, Notification notification); + + /** + * Puts the given service into background. + * @param service the service to put into background. + * @param notificationManager the notification manager to user when removing + * notifications. + * @param id the ID of the notification to be remove, or -1 if the + * notification shouldn't be removed. + */ + void stopForeground(Service service, NotificationManager notificationManager, + int id); + + /** + * Terminates the given handler thread. + * + * @param handlerThread the thread to be terminated. + * @return true whether the handler has been stopped or not. + */ + boolean stopHandlerThread(HandlerThread handlerThread); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java new file mode 100644 index 000000000..272b47218 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010 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.apps.mytracks.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.os.HandlerThread; + +/** + * The Cupcake (API level 3) specific implementation of the + * {@link ApiPlatformAdapter}. + * + * @author Bartlomiej Niechwiej + */ +public class CupcakePlatformAdapter implements ApiPlatformAdapter { + + @Override + public void startForeground(Service service, + NotificationManager notificationManager, int id, + Notification notification) { + service.setForeground(true); + notificationManager.notify(id, notification); + } + + @Override + public void stopForeground(Service service, + NotificationManager notificationManager, int id) { + service.setForeground(false); + if (id != -1) { + notificationManager.cancel(id); + } + } + + @Override + public boolean stopHandlerThread(HandlerThread handlerThread) { + // Do nothing, as Cupcake doesn't provide quit(). + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java new file mode 100644 index 000000000..9c32946d3 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2010 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.apps.mytracks.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.os.HandlerThread; + +/** + * The Eclair (API level 5) specific implementation of the + * {@link ApiPlatformAdapter}. + * + * @author Bartlomiej Niechwiej + */ +public class EclairPlatformAdapter implements ApiPlatformAdapter { + + @Override + public void startForeground(Service service, + NotificationManager notificationManager, int id, + Notification notification) { + service.startForeground(id, notification); + } + + @Override + public void stopForeground(Service service, + NotificationManager notificationManager, int id) { + service.stopForeground(id != -1); + } + + @Override + public boolean stopHandlerThread(HandlerThread handlerThread) { + return handlerThread.quit(); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java new file mode 100644 index 000000000..855619cb7 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2010 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.apps.mytracks.services; + +import android.content.Context; +import android.test.AndroidTestCase; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Tests {@link DefaultTrackNameFactory} + * + * @author Matthew Simmons + */ +public class DefaultTrackNameFactoryTest extends AndroidTestCase { + /** + * A version of the factory which allows us to supply our own answer as to + * whether a timestamp-based track name should be used. + */ + private static class MockDefaultTrackNameFactory + extends DefaultTrackNameFactory { + private final boolean useTimestamp; + + MockDefaultTrackNameFactory(Context context, boolean useTimestamp) { + super(context); + this.useTimestamp = useTimestamp; + } + + @Override + protected boolean useTimestampTrackName() { + return useTimestamp; + } + } + + private static final long TIMESTAMP = 1288213406000L; + + public void testTimestampTrackName() { + DefaultTrackNameFactory factory = + new MockDefaultTrackNameFactory(getContext(), true); + + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + assertEquals(formatter.format(new Date(TIMESTAMP)), + factory.newTrackName(1, TIMESTAMP)); + } + + public void testIncrementingTrackName() { + DefaultTrackNameFactory factory = + new MockDefaultTrackNameFactory(getContext(), false); + + assertEquals("Track 1", factory.newTrackName(1, TIMESTAMP)); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 8a768f637..fc7ae5396 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -15,12 +15,33 @@ */ package com.google.android.apps.mytracks.services; +import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME; + +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.MyTracksProvider; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.maps.mytracks.R; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.location.Location; import android.os.IBinder; +import android.test.RenamingDelegatingContext; import android.test.ServiceTestCase; +import android.test.mock.MockContentResolver; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; +import java.util.List; + /** * Tests for the MyTracks track recording service. * @@ -28,24 +49,513 @@ import android.test.suitebuilder.annotation.SmallTest; */ public class TrackRecordingServiceTest extends ServiceTestCase { - + + private Context context; + private MyTracksProviderUtils providerUtils; + private SharedPreferences sharedPreferences; + public TrackRecordingServiceTest() { super(TrackRecordingService.class); } + + /** + * A context wrapper with the user provided {@link ContentResolver}. + */ + private static class MockContext extends ContextWrapper { + private final ContentResolver contentResolver; + + public MockContext(ContentResolver contentResolver, Context base) { + super(base); + this.contentResolver = contentResolver; + } + + @Override + public ContentResolver getContentResolver() { + return contentResolver; + } + } + + /** + * A mock class that forces API level < 5 to make sure we can workaround a bug + * in ServiceTestCase (throwing a NPE). + * See http://code.google.com/p/android/issues/detail?id=12122 for more + * details. + */ + private static class MockApiFeatures extends ApiFeatures { + @Override + protected int getApiLevel() { + return 4; + } + } + @Override + protected void setUp() throws Exception { + super.setUp(); + + ApiFeatures.injectInstance(new MockApiFeatures()); + + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + context = new MockContext(mockContentResolver, targetContext); + MyTracksProvider provider = new MyTracksProvider(); + provider.attachInfo(context, null); + mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); + setContext(context); + + providerUtils = MyTracksProviderUtils.Factory.get(context); + + sharedPreferences = context.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + // Disable auto resume by default. + updateAutoResumePrefs(0, -1); + // No recording track. + Editor editor = sharedPreferences.edit(); + editor.putLong(context.getString(R.string.recording_track_key), -1); + editor.commit(); + } + @SmallTest public void testStartable() { - Intent startIntent = new Intent(); - startIntent.setClass(getContext(), TrackRecordingService.class); - startService(startIntent); + startService(createStartIntent()); assertNotNull(getService()); } @MediumTest public void testBindable() { - Intent startIntent = new Intent(); - startIntent.setClass(getContext(), TrackRecordingService.class); - IBinder service = bindService(startIntent); + IBinder service = bindService(createStartIntent()); assertNotNull(service); } + + @MediumTest + public void testResumeAfterReboot_shouldResume() throws Exception { + // Insert a dummy track and mark it as recording track. + createDummyTrack(123, System.currentTimeMillis(), true); + + // Clear the number of attempts and set the timeout to 10 min. + updateAutoResumePrefs(0, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We expect to resume the previous track. + assertTrue(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(123, service.getRecordingTrackId()); + } + + @MediumTest + public void testResumeAfterReboot_simulateReboot() throws Exception { + updateAutoResumePrefs(0, 10); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Simulate recording a track. + long id = service.startNewTrack(); + assertTrue(service.isRecording()); + assertEquals(id, service.getRecordingTrackId()); + shutdownService(); + assertEquals(id, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), -1)); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // TODO: shutdownService() has a bug and doesn't set mServiceCreated + // to false, thus preventing from a second call to onCreate(). + // Report the bug to Android team. Until then, the following check + // must be commented out. + + // assertTrue(getService().isRecording()); + } + + @MediumTest + public void testResumeAfterReboot_noRecordingTrack() throws Exception { + // Insert a dummy track and mark it as recording track. + createDummyTrack(123, System.currentTimeMillis(), false); + + // Clear the number of attempts and set the timeout to 10 min. + updateAutoResumePrefs(0, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We don't expect to resume the previous track, because it was stopped. + assertFalse(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testResumeAfterReboot_expiredTrack() throws Exception { + // Insert a dummy track last updated 20 min ago. + createDummyTrack(123, System.currentTimeMillis() - 20 * 60 * 1000, true); + + // Clear the number of attempts and set the timeout to 10 min. + updateAutoResumePrefs(0, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We don't expect to resume the previous track, because it has expired. + assertFalse(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testResumeAfterReboot_tooManyAttempts() throws Exception { + // Insert a dummy track. + createDummyTrack(123, System.currentTimeMillis(), true); + + // Set the number of attempts to max. + updateAutoResumePrefs( + TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We don't expect to resume the previous track, because there were already + // too many attempts. + assertFalse(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testRecording_noTracks() throws Exception { + List tracks = providerUtils.getAllTracks(); + assertTrue(tracks.isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + // Test if we start in no-recording mode by default. + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testRecording_oldTracks() throws Exception { + createDummyTrack(123, -1, false); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testRecording_orphanedRecordingTrack() throws Exception { + // Just set recording track to a bogus value. + setRecordingTrack(256); + + // Make sure that the service will not start recording and will clear + // the bogus track. + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testStartNewTrack_noRecording() throws Exception { + List tracks = providerUtils.getAllTracks(); + assertTrue(tracks.isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + long id = service.startNewTrack(); + assertTrue(id >= 0); + assertTrue(service.isRecording()); + Track track = providerUtils.getTrack(id); + assertNotNull(track); + assertEquals(id, track.getId()); + assertEquals(id, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), -1)); + assertEquals(id, service.getRecordingTrackId()); + } + + @MediumTest + public void testStartNewTrack_alreadyRecording() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + try { + service.startNewTrack(); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + assertEquals(123, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), 0)); + assertEquals(123, service.getRecordingTrackId()); + } + + @MediumTest + public void testEndCurrentTrack_alreadyRecording() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + // End the current track. + service.endCurrentTrack(); + assertFalse(service.isRecording()); + assertEquals(-1, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), 0)); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testEndCurrentTrack_noRecording() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // End the current track. + try { + service.endCurrentTrack(); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + assertEquals(-1, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), 0)); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testIntegration_completeRecordingSession() throws Exception { + List tracks = providerUtils.getAllTracks(); + assertTrue(tracks.isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Start a track. + long id = service.startNewTrack(); + assertTrue(id >= 0); + assertTrue(service.isRecording()); + Track track = providerUtils.getTrack(id); + assertNotNull(track); + assertEquals(id, track.getId()); + assertEquals(id, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), -1)); + assertEquals(id, service.getRecordingTrackId()); + + // Stop the track. Validate if it has correct data. + service.endCurrentTrack(); + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + track = providerUtils.getTrack(id); + assertNotNull(track); + assertEquals(id, track.getId()); + TripStatistics tripStatistics = track.getStatistics(); + assertNotNull(tripStatistics); + assertTrue(tripStatistics.getStartTime() > 0); + assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime()); + } + + @MediumTest + public void testDeleteAllTracks_noRecording() throws Exception { + createDummyTrack(123, -1, false); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Deleting all tracks should succeed. + service.deleteAllTracks(); + assertFalse(service.isRecording()); + assertTrue(providerUtils.getAllTracks().isEmpty()); + } + + @MediumTest + public void testDeleteAllTracks_noTracks() throws Exception { + assertTrue(providerUtils.getAllTracks().isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Deleting all tracks should succeed. + service.deleteAllTracks(); + assertFalse(service.isRecording()); + assertTrue(providerUtils.getAllTracks().isEmpty()); + } + + @MediumTest + public void testDeleteAllTracks_trackInProgress() throws Exception { + createDummyTrack(123, -1, true); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + // Since we have a track in progress, we expect to fail. + try { + service.deleteAllTracks(); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + assertTrue(service.isRecording()); + assertEquals(1, providerUtils.getAllTracks().size()); + } + + @MediumTest + public void testHasRecorded_noTracks() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertFalse(service.hasRecorded()); + } + + @MediumTest + public void testHasRecorded_trackInProgress() throws Exception { + createDummyTrack(123, -1, true); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + assertTrue(service.hasRecorded()); + } + + @MediumTest + public void testHasRecorded_oldTracks() throws Exception { + createDummyTrack(123, -1, false); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertTrue(service.hasRecorded()); + } + + @MediumTest + public void testInsertStatisticsMarker_noRecordingTrack() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + Location loc = new Location("gps"); + try { + service.insertStatisticsMarker(loc); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + } + + @MediumTest + public void testInsertStatisticsMarker_validLocation() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + Location loc = new Location("gps"); + assertEquals(1, service.insertStatisticsMarker(loc)); + assertEquals(2, service.insertStatisticsMarker(loc)); + + // TODO: Add more checks. + } + + @MediumTest + public void testInsertWaypointMarker_noRecordingTrack() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + Location loc = new Location("gps"); + Waypoint waypoint = new Waypoint(); + waypoint.setId(1); + waypoint.setLocation(loc); + try { + service.insertWaypointMarker(waypoint); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + } + + @MediumTest + public void testInsertWaypointMarker_invalidWaypoint() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + Waypoint waypoint = new Waypoint(); + assertEquals(-1, service.insertWaypointMarker(waypoint)); + } + + @MediumTest + public void testInsertWaypointMarker_validWaypoint() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + Location loc = new Location("gps"); + Waypoint waypoint = new Waypoint(); + waypoint.setId(1); + waypoint.setLocation(loc); + assertEquals(1, service.insertWaypointMarker(waypoint)); + } + + private ITrackRecordingService bindAndGetService(Intent intent) { + ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( + bindService(intent)); + assertNotNull(service); + return service; + } + + private Track createDummyTrack(long id, long stopTime, boolean isRecording) { + Track dummyTrack = new Track(); + dummyTrack.setId(id); + dummyTrack.setName("Dummy Track"); + TripStatistics tripStatistics = new TripStatistics(); + tripStatistics.setStopTime(stopTime); + dummyTrack.setStatistics(tripStatistics); + addTrack(dummyTrack, isRecording); + return dummyTrack; + } + + private void updateAutoResumePrefs(int attempts, int timeoutMins) { + Editor editor = sharedPreferences.edit(); + editor.putInt(context.getString( + R.string.auto_resume_track_current_retry_key), attempts); + editor.putInt(context.getString( + R.string.auto_resume_track_timeout_key), timeoutMins); + editor.commit(); + } + + private Intent createStartIntent() { + Intent startIntent = new Intent(); + startIntent.setClass(context, TrackRecordingService.class); + return startIntent; + } + + private void addTrack(Track track, boolean isRecording) { + assertTrue(track.getId() >= 0); + providerUtils.insertTrack(track); + assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId()); + setRecordingTrack(isRecording ? track.getId() : -1); + } + + private void setRecordingTrack(long id) { + Editor editor = sharedPreferences.edit(); + editor.putLong(context.getString(R.string.recording_track_key), id); + editor.commit(); + } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java index f19b2da90..ccb3fa858 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java @@ -20,13 +20,11 @@ public class TripStatisticsBuilderTest extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); - builder = new TripStatisticsBuilder(); - builder.resume(); + builder = new TripStatisticsBuilder(System.currentTimeMillis()); } public void testAddLocationSimple() throws Exception { - builder = new TripStatisticsBuilder(); - builder.resumeAt(1000); + builder = new TripStatisticsBuilder(1000); TripStatistics stats = builder.getStatistics(); assertEquals(0.0, builder.getSmoothedElevation()); @@ -84,8 +82,7 @@ public class TripStatisticsBuilderTest extends TestCase { */ public void testElevationSimple() throws Exception { for (double elevation = 0; elevation < 1000; elevation += 10) { - builder = new TripStatisticsBuilder(); - builder.resume(); + builder = new TripStatisticsBuilder(System.currentTimeMillis()); for (int j = 0; j < 100; j++) { assertEquals(0.0, builder.updateElevation(elevation)); assertEquals(elevation, builder.getSmoothedElevation()); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java index 1f120cbf2..f47ab6dbb 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java @@ -75,4 +75,8 @@ public class ApiFeaturesTest extends TestCase { features.setApiLevel(8); assertTrue(features.hasModernSignalStrength()); } + + public void testGetApiPlatformAdapter() { + assertNotNull(features.getApiPlatformAdapter()); + } }