diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 98ad926ca..29af80da0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -128,7 +128,7 @@ public class MyTracks extends TabActivity implements OnTouchListener { providerUtils = MyTracksProviderUtils.Factory.get(this); preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); - dataHub = new TrackDataHub(this, preferences, providerUtils); + dataHub = TrackDataHub.getInstance(this); menuManager = new MenuManager(this); serviceBinder = TrackRecordingServiceBinder.getInstance(this); @@ -174,15 +174,6 @@ public class MyTracks extends TabActivity implements OnTouchListener { Eula.showEulaRequireAcceptance(this); } - @Override - protected void onDestroy() { - Log.d(TAG, "MyTracks.onDestroy"); - - dataHub.destroy(); - - super.onDestroy(); - } - @Override protected void onStop() { Log.d(TAG, "MyTracks.onStop"); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index 9877bfa4f..c1481cf5e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -22,6 +22,7 @@ import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.DataSourceManager.DataSourceListener; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; @@ -150,8 +151,8 @@ public class TrackDataHub { // Get content notifications on the main thread, send listener callbacks in another. // This ensures listener calls are serialized. - private final HandlerThread listenerHandlerThread; - private final Handler listenerHandler; + private HandlerThread listenerHandlerThread; + private Handler listenerHandler; /** Manager for external listeners (those from activities). */ private final TrackDataListeners listeners; @@ -192,13 +193,24 @@ public class TrackDataHub { private int lastSamplingFrequency; private DoubleBufferedLocationFactory locationFactory; - /** - * Default constructor. - */ - public TrackDataHub(Context ctx, SharedPreferences preferences, - MyTracksProviderUtils providerUtils) { - this(ctx, new DataSourcesWrapperImpl(ctx, preferences), new TrackDataListeners(), - preferences, providerUtils, TARGET_DISPLAYED_TRACK_POINTS); + private static TrackDataHub instance; + + public synchronized static TrackDataHub getInstance(Context context) { + if (instance != null) { + return instance; + } + + // Ensure our singleton is never bound to an activity, to avoid memory leaks. + context = context.getApplicationContext(); + + SharedPreferences preferences = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); + MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(context); + instance = new TrackDataHub(context, + new DataSourcesWrapperImpl(context, preferences), + new TrackDataListeners(), + preferences, providerUtils, + TARGET_DISPLAYED_TRACK_POINTS); + return instance; } /** @@ -222,10 +234,6 @@ public class TrackDataHub { METRIC_UNITS_KEY = context.getString(R.string.metric_units_key); SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key); - listenerHandlerThread = new HandlerThread("trackDataContentThread"); - listenerHandlerThread.start(); - listenerHandler = new Handler(listenerHandlerThread.getLooper()); - resetState(); } @@ -241,6 +249,10 @@ public class TrackDataHub { } started = true; + listenerHandlerThread = new HandlerThread("trackDataContentThread"); + listenerHandlerThread.start(); + listenerHandler = new Handler(listenerHandlerThread.getLooper()); + // This may or may not register internal listeners, depending on whether // we already had external listeners. dataSourceManager.updateAllListeners(getNeededListenerTypes()); @@ -250,14 +262,6 @@ public class TrackDataHub { loadDataForAllListeners(); } - private void loadSharedPreferences() { - selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); - useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); - reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); - minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, - DEFAULT_MIN_REQUIRED_ACCURACY); - } - /** * Stops listening to data sources and reporting the data to external * listeners. @@ -273,16 +277,27 @@ public class TrackDataHub { dataSourceManager.unregisterAllListeners(); started = false; + + listenerHandlerThread.getLooper().quit(); + listenerHandlerThread = null; + listenerHandler = null; } - /** Permanently invalidates and throws away all resources used by this class. */ - public void destroy() { - if (started) { - throw new IllegalStateException("Can only destroy the data hub after it's been stopped"); + @Override + protected void finalize() throws Throwable { + if (started || listenerHandlerThread.isAlive()) { + Log.e(TAG, "Forgot to stop() TrackDataHub"); } + + super.finalize(); + } - ApiFeatures.getInstance().getApiPlatformAdapter() - .stopHandlerThread(listenerHandlerThread); + private void loadSharedPreferences() { + selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); + useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); + reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); + minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, + DEFAULT_MIN_REQUIRED_ACCURACY); } /** Updates known magnetic declination if needed. */ @@ -325,6 +340,10 @@ public class TrackDataHub { * is not available or doesn't have a fix. */ public void forceUpdateLocation() { + if (!started) { + Log.w(TAG, "Not started, not forcing location update"); + return; + } Log.i(TAG, "Forcing location update"); Location loc = dataSources.getLastKnownLocation(); @@ -1059,6 +1078,12 @@ public class TrackDataHub { // @VisibleForTesting protected void runInListenerThread(Runnable runnable) { + if (listenerHandler == null) { + // Use a Throwable to ensure the stack trace is logged. + Log.e(TAG, "Tried to use listener thread before start()", new Throwable()); + return; + } + listenerHandler.post(runnable); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java index 9f69c79f5..fcf758ac6 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -124,7 +124,7 @@ public class TrackDataHubTest extends AndroidTestCase { AndroidMock.replay(dataSources); hub.stop(); - hub.destroy(); + hub = null; super.tearDown(); }