From 48d8aa4cd800868f22d41182dee5274475dc6f11 Mon Sep 17 00:00:00 2001 From: Bartlomiej Niechwiej Date: Sun, 24 Oct 2010 21:24:11 -0700 Subject: [PATCH] Introduce an interface to decouple platform specific API from the classes that require it. Also: 1) Put the service into foreground to prevent from being killed by the system 2) Fix a bug the prevented MyTracks from running on pre-Eclair devices --- .../android/apps/mytracks/MyTracksMap.java | 3 +- .../services/TrackRecordingService.java | 8 ++- .../apps/mytracks/util/ApiFeatures.java | 38 ++++++++++-- .../mytracks/util/ApiPlatformAdapter.java | 60 +++++++++++++++++++ .../mytracks/util/CupcakePlatformAdapter.java | 53 ++++++++++++++++ .../mytracks/util/EclairPlatformAdapter.java | 48 +++++++++++++++ .../services/TrackRecordingServiceTest.java | 19 ++++++ .../apps/mytracks/util/ApiFeaturesTest.java | 4 ++ 8 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java create mode 100644 MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java create mode 100644 MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index 2785f41dc..635f7d9da 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -379,7 +379,8 @@ public class MyTracksMap extends MapActivity protected void onDestroy() { Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy"); if (updateTrackThread != null) { - updateTrackThread.quit(); + ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread( + updateTrackThread); } super.onDestroy(); 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 b5eb7b914..ca080bd2f 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; @@ -329,6 +330,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 +342,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); } } 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/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 127b6a285..de4b2b8ab 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -23,6 +23,7 @@ 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; @@ -57,6 +58,9 @@ public class TrackRecordingServiceTest super(TrackRecordingService.class); } + /** + * A context wrapper with the user provided {@link ContentResolver}. + */ private static class MockContext extends ContextWrapper { private final ContentResolver contentResolver; @@ -70,11 +74,26 @@ public class TrackRecordingServiceTest 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."); 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()); + } }