diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 3e72ff098..2b6362942 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -13,7 +13,6 @@ android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" /> + + + + + + + + + + + + + + + diff --git a/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java index c0f95242a..c4e4e0728 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java @@ -35,7 +35,6 @@ public class AggregatedStatsActivity extends Activity implements public AggregatedStatsActivity() { this.utils = new StatsUtilities(this); - this.tracksProvider = MyTracksProviderUtils.Factory.get(this); } @Override @@ -58,6 +57,8 @@ public class AggregatedStatsActivity extends Activity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + this.tracksProvider = MyTracksProviderUtils.Factory.get(this); + // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.stats); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 990a9a2d9..951234d9e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -31,10 +31,10 @@ import com.google.android.apps.mytracks.io.AuthManagerFactory; import com.google.android.apps.mytracks.io.GpxImporter; import com.google.android.apps.mytracks.io.SendToDocs; import com.google.android.apps.mytracks.io.SendToMyMaps; -import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener; import com.google.android.apps.mytracks.io.TempFileCleaner; import com.google.android.apps.mytracks.io.TrackWriter; import com.google.android.apps.mytracks.io.TrackWriterFactory; +import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; @@ -173,8 +173,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, ITrackRecordingService trackRecordingService = ITrackRecordingService.Stub.asInterface(service); try { - // TODO: Send a start service intent and broadcast service started message - // to avoid the hack below and a race condition. + // TODO: Send a start service intent and broadcast service started + // message to avoid the hack below and a race condition. if (startNewTrackRequested) { startNewTrackRequested = false; startRecordingNewTrack(trackRecordingService); @@ -237,9 +237,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, } private boolean isRecordingBasedOnSharedPreferences() { - // TrackRecordingServices guarantees that recordingTrackId is set to + // TrackRecordingService guarantees that recordingTrackId is set to // -1 if the track has been stopped. - // TODO: Refresh recordingTrackId. return recordingTrackId >= 0; } @@ -256,6 +255,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, providerUtils = MyTracksProviderUtils.Factory.get(this); menuManager = new MenuManager(this); sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + dialogManager = new DialogManager(this); // The volume we want to control is the Text-To-Speech volume int volumeStream = @@ -359,7 +359,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, // to the user. Log.d(MyTracksConstants.TAG, "MyTracks.onResume"); tryBindTrackRecordingService(); - dialogManager = new DialogManager(this); super.onResume(); } @@ -693,58 +692,18 @@ public class MyTracks extends TabActivity implements OnTouchListener, } @Override - public void onSharedPreferenceChanged( - SharedPreferences sharedPreferences, String key) { - // The service itself cannot listen to changes (not supported by Android for - // services that run in a separate process). So we'll notify it manually: - if (key != null && trackRecordingService != null) { - try { - trackRecordingService.sharedPreferenceChanged(key); - } catch (RemoteException e) { - Log.w(MyTracksConstants.TAG, - "MyTracks: Cannot notify track recording service of changes " - + "to shared preferences: ", e); - } - } + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { if (key != null && key.equals(getString(R.string.selected_track_key))) { - selectedTrackId = - sharedPreferences.getLong(getString(R.string.selected_track_key), -1); + selectedTrackId = sharedPreferences.getLong( + getString(R.string.selected_track_key), -1); + } + if (key != null && key.equals(getString(R.string.recording_track_key))) { + recordingTrackId = sharedPreferences.getLong( + getString(R.string.recording_track_key), -1); } } - /** - * Simulates the recording of a random location. - * This is for debugging and testing only. Useful if there is no GPS signal - * available. - */ -// public void recordRandomLocation() { -// if (trackRecordingService != null) { -// Location loc = new Location("gps"); -// double latitude = 37.5 + random.nextDouble() / 1000; -// double longitude = -120.0 + random.nextDouble() / 1000; -// loc.setLatitude(latitude); -// loc.setLongitude(longitude); -// loc.setAltitude(random.nextDouble() * 100); -// loc.setTime(System.currentTimeMillis()); -// loc.setSpeed(random.nextFloat()); -// MyTracksMap map = -// (MyTracksMap) getLocalActivityManager().getActivity("tab1"); -// if (map != null) { -// map.onLocationChanged(loc); -// } -// StatsActivity stats = -// (StatsActivity) getLocalActivityManager().getActivity("tab2"); -// if (stats != null) { -// stats.onLocationChanged(loc); -// } -// try { -// trackRecordingService.recordLocation(loc); -// } catch (RemoteException e) { -// Log.e(MyTracksConstants.TAG, "MyTracks", e); -// } -// } -// } - /** * Resets status information for sending to MyMaps/Docs. */ @@ -1053,8 +1012,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, ITrackRecordingService trackRecordingService) { try { recordingTrackId = trackRecordingService.startNewTrack(); - // TODO: This is a hack to propagate recordingTrackId in multiprocess env. - setRecordingTrackId(recordingTrackId); // Select the recording track. setSelectedTrackId(recordingTrackId); Toast.makeText(this, getString(R.string.status_now_recording), @@ -1088,16 +1045,16 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ public void stopRecording() { if (trackRecordingService != null) { + // Save the track id as the shared preference will overwrite the recording track id. + long currentTrackId = recordingTrackId; try { trackRecordingService.endCurrentTrack(); } catch (RemoteException e) { Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e); } Intent intent = new Intent(MyTracks.this, MyTracksDetails.class); - intent.putExtra("trackid", recordingTrackId); + intent.putExtra("trackid", currentTrackId); intent.putExtra("hasCancelButton", false); - // TODO: This is a hack to propagate recordingTrackId in multiprocess env. - setRecordingTrackId(recordingTrackId = -1); startActivity(intent); } tryUnbindTrackRecordingService(); @@ -1148,29 +1105,16 @@ public class MyTracks extends TabActivity implements OnTouchListener, * @param trackId the id of the track */ public void setSelectedTrackId(final long trackId) { - runOnUiThread(new Runnable() { - public void run() { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putLong(getString(R.string.selected_track_key), trackId); - editor.commit(); - } - }); + sharedPreferences + .edit() + .putLong(getString(R.string.selected_track_key), trackId) + .commit(); } long getSelectedTrackId() { return selectedTrackId; } - private void setRecordingTrackId(final long trackId) { - runOnUiThread(new Runnable() { - public void run() { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putLong(getString(R.string.recording_track_key), trackId); - editor.commit(); - } - }); - } - /** * Binds to track recording service if it is running. */ @@ -1335,4 +1279,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, static void clearInstance() { instance = null; } + + // @VisibleForTesting + ITrackRecordingService getTrackRecordingService() { + return trackRecordingService; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java index 201578f5e..e58c99388 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java @@ -26,10 +26,10 @@ import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; /** * An activity that let's the user see and edit the settings. @@ -158,7 +158,7 @@ public class MyTracksSettings extends PreferenceActivity { getPreferenceManager().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(backupListener); - super.onPause(); + super.onDestroy(); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java index 448b820d1..10067ba08 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java @@ -209,7 +209,7 @@ public class SendToDocs { androidClient = GDataClientFactory.getGDataClient(activity); SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); - wiseWrapper.setClient(androidClient, gdataClient); + wiseWrapper.setClient(gdataClient); Log.d(MyTracksConstants.TAG, "GData connection prepared: " + this.writelyAuth); String sheetTitle = "My Tracks"; @@ -220,7 +220,7 @@ public class SendToDocs { DocumentsClient docsGdataClient = new DocumentsClient(androidClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); - writelyWrapper.setClient(androidClient, docsGdataClient); + writelyWrapper.setClient(docsGdataClient); // First try to find the spreadsheet: if (!getSpreadsheetId(writelyWrapper, sheetTitle)) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java index 07f700007..97d7cc99e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java @@ -17,7 +17,6 @@ package com.google.android.apps.mytracks.io.gdata; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.io.AuthManager; -import com.google.wireless.gdata.client.GDataClient; import com.google.wireless.gdata.client.GDataServiceClient; import com.google.wireless.gdata.client.HttpException; import com.google.wireless.gdata.parser.ParseException; @@ -28,6 +27,10 @@ import android.util.Log; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** @@ -83,44 +86,64 @@ public class GDataWrapper { public static final int ERROR_CLEANED_UP = 7; // An unknown error occurred. public static final int ERROR_UNKNOWN = 100; + + private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1; + private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000; private String errorMessage; private int errorType; - private GDataClient androidGDataClient; private GDataServiceClient gdataServiceClient; private AuthManager auth; private boolean retryOnAuthFailure; - private int retriesPending; - private boolean cleanupCalled; - + public GDataWrapper() { errorType = ERROR_NO_ERROR; errorMessage = null; auth = null; retryOnAuthFailure = false; - retriesPending = 0; - cleanupCalled = false; } - public void setClient(GDataClient androidGDataClient, - GDataServiceClient gdataServiceClient) { - this.androidGDataClient = androidGDataClient; + public void setClient(GDataServiceClient gdataServiceClient) { this.gdataServiceClient = gdataServiceClient; } public boolean runAuthenticatedFunction( final AuthenticatedFunction function) { - return runOne(function, null); + return runCommon(function, null); } public boolean runQuery(final QueryFunction query) { - return runOne(null, query); + return runCommon(null, query); } /** * Runs an arbitrary piece of code. */ - private boolean runOne(final AuthenticatedFunction function, + private boolean runCommon(final AuthenticatedFunction function, + final QueryFunction query) { + for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) { + runOne(function, query); + if (errorType == ERROR_NO_ERROR) { + return true; + } + + Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage); + if (errorType == ERROR_AUTH && auth != null) { + if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) { + return false; + } + } + + Log.d(MyTracksConstants.TAG, "retrying function/query"); + } + return false; + } + + /** + * Execute a given function or query. If one is executed, errorType and + * errorMessage will contain the result/status of the function/query. + */ + private void runOne(final AuthenticatedFunction function, final QueryFunction query) { try { if (function != null) { @@ -128,11 +151,14 @@ public class GDataWrapper { } else if (query != null) { query.query(gdataServiceClient); } else { - return false; + throw new IllegalArgumentException( + "invalid invocation of runOne; one of function/query " + + "must be non-null"); } + errorType = ERROR_NO_ERROR; errorMessage = null; - return true; + } catch (AuthenticationException e) { Log.e(MyTracksConstants.TAG, "Exception", e); errorType = ERROR_AUTH; @@ -166,32 +192,42 @@ public class GDataWrapper { errorType = ERROR_CONFLICT; errorMessage = e.getMessage(); } + } - Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage); - if (errorType == ERROR_AUTH && auth != null) { - Runnable whenFinished = null; - if (retryOnAuthFailure) { - retriesPending++; - // This is a little odd. This method cannot return in the the thread - // that called the original runQuery(). We should look at reworking this - // API. - whenFinished = new Runnable() { - public void run() { - retriesPending--; - try { - runOne(function, query); - } catch (IllegalStateException ise) { - // This can happen if the connection pool was shut down. - Log.e(MyTracksConstants.TAG, "Failed to rerun query.", ise); - } - if (cleanupCalled && retriesPending == 0) { - cleanUp(); - } - } - }; - } - auth.invalidateAndRefresh(whenFinished); + /** + * Invalidates and refreshes the auth token. Blocks until the refresh has + * completed or until we deem the refresh as having timed out. + * + * @return true If the invalidate/refresh succeeds, false if it fails or + * times out. + */ + private boolean invalidateAndRefreshAuthToken() { + Log.d(MyTracksConstants.TAG, "Retrying due to auth failure"); + // This FutureTask doesn't do anything -- it exists simply to be + // blocked upon using get(). + FutureTask whenFinishedFuture = new FutureTask(new Runnable() { + public void run() {} + }, null); + + auth.invalidateAndRefresh(whenFinishedFuture); + + try { + Log.d(MyTracksConstants.TAG, "waiting for invalidate"); + whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT, + TimeUnit.MILLISECONDS); + Log.d(MyTracksConstants.TAG, "invalidate finished"); + return true; + + } catch (InterruptedException e) { + Log.e(MyTracksConstants.TAG, "Failed to invalidate", e); + } catch (ExecutionException e) { + Log.e(MyTracksConstants.TAG, "Failed to invalidate", e); + } catch (TimeoutException e) { + Log.e(MyTracksConstants.TAG, "Invalidate didn't complete in time", e); + } finally { + whenFinishedFuture.cancel(false); } + return false; } @@ -203,21 +239,10 @@ public class GDataWrapper { return errorMessage; } - // cleanUp must be called when done using this wrapper to close the client. - // Note that the cleanup will be delayed if auth failure retries were - // requested and there is a pending retry. - public void cleanUp() { - cleanupCalled = true; - if (retriesPending == 0) { - androidGDataClient.close(); - androidGDataClient = null; - } - } - public void setAuthManager(AuthManager auth) { this.auth = auth; } - + public void setRetryOnAuthFailure(boolean retry) { retryOnAuthFailure = retry; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java index 802199460..79d2f5328 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java @@ -20,7 +20,6 @@ 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; diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl b/MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl index c13e04d1e..d0e492563 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl +++ b/MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl @@ -84,13 +84,4 @@ interface ITrackRecordingService { * Deletes all the stored tracks. */ void deleteAllTracks(); - - /** - * Notifies the service that its preferences may have been changed. - * This is necessary because the service running on a separate process cannot - * listen to the changes itself. - * - * @param key the preference key which may have changed - */ - void sharedPreferenceChanged(in String key); } 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 edfafc3e4..2fe7e5c33 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -20,7 +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.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.util.Log; /** @@ -28,8 +28,9 @@ import android.util.Log; * * @author Sandor Dornbush */ -public class PreferenceManager { +public class PreferenceManager implements OnSharedPreferenceChangeListener { private TrackRecordingService service; + private SharedPreferences sharedPreferences; private final String announcementFrequencyKey; private final String autoResumeTrackCurrentRetryKey; private final String autoResumeTrackTimeoutKey; @@ -44,6 +45,14 @@ public class PreferenceManager { 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"); + } + sharedPreferences.registerOnSharedPreferenceChangeListener(this); announcementFrequencyKey = service.getString(R.string.announcement_frequency_key); @@ -67,17 +76,9 @@ public class PreferenceManager { service.getString(R.string.signal_sampling_frequency_key); splitFrequencyKey = service.getString(R.string.split_frequency_key); - } - - private SharedPreferences getSharedPreference() { - SharedPreferences 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"); - } - return sharedPreferences; + + // Refresh all properties. + onSharedPreferenceChanged(sharedPreferences, null); } /** @@ -86,11 +87,9 @@ public class PreferenceManager { * * @param key the key that changed (may be null to update all preferences) */ - public void onSharedPreferenceChanged(String key) { - // This is a hack! - // Ideally we should not have to reload the shared pref here. - // We have to reload it since the in memory version will not match the values from the activity. - SharedPreferences sharedPreferences = getSharedPreference(); + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { if (key == null || key.equals(minRecordingDistanceKey)) { service.setMinRecordingDistance( sharedPreferences.getInt( @@ -169,16 +168,16 @@ public class PreferenceManager { } public void setAutoResumeTrackCurrentRetry(int retryAttempts) { - SharedPreferences sharedPreferences = getSharedPreference(); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts); - editor.commit(); + sharedPreferences + .edit() + .putInt(autoResumeTrackCurrentRetryKey, retryAttempts) + .commit(); } public void setRecordingTrack(long id) { - SharedPreferences sharedPreferences = getSharedPreference(); - Editor editor = sharedPreferences.edit(); - editor.putLong(recordingTrackKey, id); - editor.commit(); + sharedPreferences + .edit() + .putLong(recordingTrackKey, id) + .commit(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java index 39f4c61ee..79533e202 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java @@ -125,6 +125,8 @@ public class SplitManager { * @param splitFrequency The frequency in time or distance */ public void setSplitFrequency(int splitFrequency) { + Log.d(MyTracksConstants.TAG, + "setSplitFrequency: splitFrequency = " + splitFrequency); this.splitFrequency = splitFrequency; // TODO: Decouple service from this class once and forever. diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java index ce339b3f6..9ee8b6c72 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java @@ -34,7 +34,7 @@ import android.util.Log; import java.util.Locale; /** - * This class will periodically announce the user's trip statitics. + * This class will periodically announce the user's trip statistics. * * @author Sandor Dornbush */ @@ -98,7 +98,7 @@ public class StatusAnnouncerTask implements PeriodicTask { * Called when the TTS engine is initialized. */ private void onTtsInit(int status) { - Log.i(MyTracksConstants.TAG, "StatusAnnouncerTask.TTS init: " + status); + Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status); this.ready = (status == TextToSpeech.SUCCESS); if (ready) { @@ -261,7 +261,9 @@ public class StatusAnnouncerTask implements PeriodicTask { protected void listenToPhoneState(PhoneStateListener listener, int events) { TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - telephony.listen(listener, events); + if (telephony != null) { + telephony.listen(listener, events); + } } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java index 5c83e64f5..432d0ddab 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java @@ -15,24 +15,23 @@ */ package com.google.android.apps.mytracks.services; -import android.util.Log; - import com.google.android.apps.mytracks.MyTracksConstants; +import android.util.Log; + /** - * This class will manage a period task executer. + * This class manages a period task executer. * * @author Sandor Dornbush */ public class TaskExecuterManager { - int frequency; - PeriodicTask task; - PeriodicTaskExecuter executer; + private int frequency; + private final PeriodicTask task; + private PeriodicTaskExecuter executer; - public TaskExecuterManager(int frequency, - PeriodicTask task, - TrackRecordingService service) { + public TaskExecuterManager(int frequency, PeriodicTask task, + TrackRecordingService service) { this.task = task; setFrequency(frequency, service); } @@ -50,7 +49,7 @@ public class TaskExecuterManager { */ public void setFrequency(int frequency, TrackRecordingService service) { this.frequency = frequency; - Log.i(MyTracksConstants.TAG, "Frequency " + task.getClass().getSimpleName() + " set to: " + frequency); + Log.i(MyTracksConstants.TAG, "Frequency set to: " + frequency); if (frequency == -1) { if (executer != null) { @@ -69,7 +68,7 @@ public class TaskExecuterManager { } /** - * Restore the task at the current frequency. + * Restores the task at the current frequency. */ public void restore() { if (frequency > 0) { @@ -77,4 +76,13 @@ public class TaskExecuterManager { executer.scheduleTask(frequency * 60000); } } + + /** + * Shuts down this executer. + */ + public void shutdown() { + if (executer != null) { + executer.shutdown(); + } + } } 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 e5c7dab3a..4b7adc9de 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -103,7 +103,7 @@ public class TrackRecordingService extends Service implements LocationListener { * recorded points (as compared to each location fix). It's used to overlay * waypoints precisely in the elevation profile chart. */ - private double length = 0; + private double length; /** * Status announcer executer. @@ -118,7 +118,7 @@ public class TrackRecordingService extends Service implements LocationListener { * The interval in milliseconds that we have requested to be notified of gps * readings. */ - private long currentRecordingInterval = 0; + private long currentRecordingInterval; /** * The policy used to decide how often we should request gps updates. @@ -166,17 +166,17 @@ public class TrackRecordingService extends Service implements LocationListener { /** * Is the service currently recording a track? */ - private boolean isRecording = false; + private boolean isRecording; /** * Last good location the service has received from the location listener */ - private Location lastLocation = null; + private Location lastLocation; /** * Last valid location (i.e. not a marker) that was recorded. */ - private Location lastValidLocation = null; + private Location lastValidLocation; /** * The frequency of status announcements. @@ -295,7 +295,7 @@ public class TrackRecordingService extends Service implements LocationListener { * Tries to acquire a partial wake lock if not already acquired. Logs errors * and gives up trying in case the wake lock cannot be acquired. */ - public void acquireWakeLock() { + private void acquireWakeLock() { try { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); if (pm == null) { @@ -326,6 +326,16 @@ public class TrackRecordingService extends Service implements LocationListener { } } + /** + * Releases the wake lock if it's currently held. + */ + private void releaseWakeLock() { + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + wakeLock = null; + } + } + /** * Shows the notification message and icon in the notification bar. */ @@ -605,28 +615,6 @@ public class TrackRecordingService extends Service implements LocationListener { // Do nothing } - /* - * SharedPreferencesChangeListener interface implementation. Note that - * services don't currently receive this event (Android platform limitation). - * This should be called from an activity whenever settings change. - */ - - /** - * Notifies that preferences have changed. - * Call this with key == null to update all preferences in one call. - * - * @param key the key that changed (may be null to update all preferences) - */ - public void onSharedPreferenceChanged(String key) { - Log.d(MyTracksConstants.TAG, - "TrackRecordingService.onSharedPreferenceChanged"); - prefManager.onSharedPreferenceChanged(key); - - if (isRecording) { - registerLocationListener(); - } - } - /* * Application lifetime events: ============================ */ @@ -647,9 +635,7 @@ public class TrackRecordingService extends Service implements LocationListener { new TaskExecuterManager(-1, strengthTaskFactory.create(this), this); prefManager = new PreferenceManager(this); - prefManager.onSharedPreferenceChanged(null); registerLocationListener(); - acquireWakeLock(); /** * After 5 min, check every minute that location listener still is * registered and spit out additional debugging info to the logs: @@ -681,24 +667,25 @@ public class TrackRecordingService extends Service implements LocationListener { private void setUpAnnouncer() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.setUpAnnouncer: " + announcementExecuter); - if (announcementFrequency == -1 || recordingTrackId == -1) { - shutdownAnnouncer(); - return; - } - handler.post(new Runnable() { - public void run() { - if (announcementExecuter == null) { - StatusAnnouncerFactory statusAnnouncerFactory = - new StatusAnnouncerFactory(ApiFeatures.getInstance()); - PeriodicTask announcer = statusAnnouncerFactory.create(TrackRecordingService.this); - if (announcer == null) return; - - announcer.start(); - announcementExecuter = new PeriodicTaskExecuter(announcer, TrackRecordingService.this); + if (announcementFrequency != -1 && recordingTrackId != -1) { + handler.post(new Runnable() { + @Override + public void run() { + if (announcementExecuter == null) { + StatusAnnouncerFactory statusAnnouncerFactory = + new StatusAnnouncerFactory(ApiFeatures.getInstance()); + PeriodicTask announcer = statusAnnouncerFactory.create( + TrackRecordingService.this); + if (announcer == null) { + return; + } + announcementExecuter = new PeriodicTaskExecuter(announcer, + TrackRecordingService.this); + } + announcementExecuter.scheduleTask(announcementFrequency * 60000); } - announcementExecuter.scheduleTask(announcementFrequency * 60000); - } - }); + }); + } } private void shutdownAnnouncer() { @@ -718,14 +705,13 @@ public class TrackRecordingService extends Service implements LocationListener { Log.d(MyTracksConstants.TAG, "TrackRecordingService.onDestroy"); checkLocationListener.cancel(); timer.cancel(); - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - } isRecording = false; showNotification(); unregisterLocationListener(); shutdownAnnouncer(); + signalManager.shutdown(); splitManager.shutdown(); + releaseWakeLock(); super.onDestroy(); } @@ -951,32 +937,7 @@ 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; - shutdownAnnouncer(); - Track recordingTrack = providerUtils.getTrack(recordingTrackId); - if (recordingTrack != null) { - TripStatistics stats = recordingTrack.getStatistics(); - stats.setStopTime(System.currentTimeMillis()); - stats.setTotalTime(stats.getStopTime() - stats.getStartTime()); - long lastRecordedLocationId = - providerUtils.getLastLocationId(recordingTrackId); - ContentValues values = new ContentValues(); - if (lastRecordedLocationId >= 0 - && recordingTrack.getStopId() >= 0) { - values.put(TracksColumns.STOPID, lastRecordedLocationId); - } - values.put(TracksColumns.STOPTIME, stats.getStopTime()); - values.put(TracksColumns.TOTALTIME, stats.getTotalTime()); - getContentResolver().update(TracksColumns.CONTENT_URI, values, - "_id=" + recordingTrack.getId(), null); - } - showNotification(); - prefManager.setRecordingTrack(recordingTrackId = -1); + TrackRecordingService.this.endCurrentTrack(); } @Override @@ -992,13 +953,6 @@ public class TrackRecordingService extends Service implements LocationListener { public void recordLocation(Location loc) { onLocationChanged(loc); } - - @Override - public void sharedPreferenceChanged(String key) { - Log.d(MyTracksConstants.TAG, - "TrackRecordingService.sharedPreferenceChanged: " + key); - onSharedPreferenceChanged(key); - } }; public long startNewTrack() { @@ -1008,6 +962,7 @@ public class TrackRecordingService extends Service implements LocationListener { } long startTime = System.currentTimeMillis(); + acquireWakeLock(); Track track = new Track(); TripStatistics trackStats = track.getStatistics(); @@ -1041,6 +996,36 @@ public class TrackRecordingService extends Service implements LocationListener { return recordingTrackId; } + private void endCurrentTrack() { + Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack"); + if (recordingTrackId == -1 || !isRecording) { + throw new IllegalStateException("No recording track in progress!"); + } + + shutdownAnnouncer(); + isRecording = false; + Track recordingTrack = providerUtils.getTrack(recordingTrackId); + if (recordingTrack != null) { + TripStatistics stats = recordingTrack.getStatistics(); + stats.setStopTime(System.currentTimeMillis()); + stats.setTotalTime(stats.getStopTime() - stats.getStartTime()); + long lastRecordedLocationId = + providerUtils.getLastLocationId(recordingTrackId); + ContentValues values = new ContentValues(); + if (lastRecordedLocationId >= 0 + && recordingTrack.getStopId() >= 0) { + values.put(TracksColumns.STOPID, lastRecordedLocationId); + } + values.put(TracksColumns.STOPTIME, stats.getStopTime()); + values.put(TracksColumns.TOTALTIME, stats.getTotalTime()); + getContentResolver().update(TracksColumns.CONTENT_URI, values, + "_id=" + recordingTrack.getId(), null); + } + showNotification(); + prefManager.setRecordingTrack(recordingTrackId = -1); + releaseWakeLock(); + } + public TripStatistics getTripStatistics() { return statsBuilder.getStatistics(); } @@ -1063,8 +1048,6 @@ public class TrackRecordingService extends Service implements LocationListener { public void setAnnouncementFrequency(int announcementFrequency) { this.announcementFrequency = announcementFrequency; - Log.d(MyTracksConstants.TAG, "TrackRecordingService.setAnnouncerFrequency:" - + this.announcementFrequency); if (announcementFrequency == -1) { shutdownAnnouncer(); } else { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java index 5ade845eb..545c09450 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java @@ -17,11 +17,14 @@ package com.google.android.apps.mytracks; import com.google.android.maps.mytracks.R; +import android.app.Activity; +import android.app.Instrumentation.ActivityMonitor; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; +import android.widget.Button; import java.io.File; import java.util.concurrent.TimeoutException; @@ -47,6 +50,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ @Override protected void tearDown() throws Exception { clearSelectedAndRecordingTracks(); + waitForIdle(); super.tearDown(); } @@ -134,16 +138,17 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ assertNotNull(getActivity()); assertNotNull(MyTracks.getInstance()); assertNotNull(getActivity().getSharedPreferences()); - + // Check if not recording. clearSelectedAndRecordingTracks(); waitForIdle(); + assertFalse(getActivity().isRecording()); assertEquals(-1, getActivity().getRecordingTrackId()); long selectedTrackId = getActivity().getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); - + // Start a new track. getActivity().startRecording(); long recordingTrackId = awaitRecordingStatus(5000, true); @@ -158,9 +163,26 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ assertEquals(recordingTrackId, selectedTrackId); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); + // Watch for MyTracksDetails activity. + ActivityMonitor monitor = getInstrumentation().addMonitor( + MyTracksDetails.class.getName(), null, false); + // Now, stop the track and make sure that it is still selected, but // no longer recording. getActivity().stopRecording(); + + // Check if we got back MyTracksDetails activity. + Activity activity = getInstrumentation().waitForMonitor(monitor); + assertTrue(activity instanceof MyTracksDetails); + + // TODO: Update track name and other properties and test if they were + // properly saved. + + // Simulate a click on Save button. + Button save = (Button) activity.findViewById(R.id.trackdetails_save); + save.performClick(); + + // Check the remaining properties. recordingTrackId = awaitRecordingStatus(5000, false); assertEquals(-1, recordingTrackId); assertEquals(recordingTrackId, getActivity().getRecordingTrackId()); @@ -197,10 +219,11 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ * Clears {selected,recording}TrackId in the {@link SharedPreferences}. */ private void clearSelectedAndRecordingTracks() { - // TODO: Consider clearing all preferences. Editor editor = getActivity().getSharedPreferences().edit(); editor.putLong(getActivity().getString(R.string.selected_track_key), -1); editor.putLong(getActivity().getString(R.string.recording_track_key), -1); + + editor.clear(); editor.commit(); } 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 1fa7a581d..ecc598ad8 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -600,9 +600,26 @@ public class TrackRecordingServiceTest functionalTest(R.string.metric_units_key, false); } - // TODO: Add the following tests: - // R.string.min_recording_interval_key - // R.string.min_required_accuracy_key + @MediumTest + public void testWithProperties_noMinRecordingInterval() throws Exception { + functionalTest(R.string.min_recording_interval_key, (Object) null); + } + + @MediumTest + public void testWithProperties_defaultMinRecordingInterval() + throws Exception { + functionalTest(R.string.min_recording_interval_key, 3); + } + + @MediumTest + public void testWithProperties_noMinRequiredAccuracy() throws Exception { + functionalTest(R.string.min_required_accuracy_key, (Object) null); + } + + @MediumTest + public void testWithProperties_defaultMinRequiredAccuracy() throws Exception { + functionalTest(R.string.min_required_accuracy_key, 500); + } private ITrackRecordingService bindAndGetService(Intent intent) { ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( @@ -691,8 +708,27 @@ public class TrackRecordingServiceTest assertEquals(id, sharedPreferences.getLong( context.getString(R.string.recording_track_key), -1)); assertEquals(id, service.getRecordingTrackId()); - - // TODO: Add a few locations, insert markers, etc. + + // Insert a few points, markers and statistics. + long startTime = System.currentTimeMillis(); + for (int i = 0; i < 30; i++) { + Location loc = new Location("gps"); + loc.setLongitude(35.0f + i / 10.0f); + loc.setLatitude(45.0f - i / 5.0f); + loc.setAccuracy(5); + loc.setSpeed(10); + loc.setTime(startTime + i * 10000); + loc.setBearing(3.0f); + service.recordLocation(loc); + + if (i % 10 == 0) { + service.insertStatisticsMarker(loc); + } else if (i % 7 == 0) { + Waypoint waypoint = new Waypoint(); + waypoint.setLocation(loc); + service.insertWaypointMarker(waypoint); + } + } // Stop the track. Validate if it has correct data. service.endCurrentTrack();