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