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
This commit is contained in:
Bartlomiej Niechwiej
2010-10-24 21:24:11 -07:00
parent 95ff0d3db6
commit 48d8aa4cd8
8 changed files with 226 additions and 7 deletions
@@ -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();
@@ -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);
}
}
@@ -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;
@@ -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);
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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.");
@@ -75,4 +75,8 @@ public class ApiFeaturesTest extends TestCase {
features.setApiLevel(8);
assertTrue(features.hasModernSignalStrength());
}
public void testGetApiPlatformAdapter() {
assertNotNull(features.getApiPlatformAdapter());
}
}