Changing service connection to be a non-singleton, and properly handle its state.

This commit is contained in:
Rodrigo Damazio
2011-06-22 16:35:40 -03:00
parent e909616aff
commit 862736bba4
11 changed files with 423 additions and 432 deletions
@@ -25,7 +25,7 @@ import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceBinder;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.SystemUtils;
@@ -64,11 +64,16 @@ import android.widget.Toast;
public class MyTracks extends TabActivity implements OnTouchListener {
private TrackDataHub dataHub;
/*
/**
* Menu manager.
*/
private MenuManager menuManager;
/**
* Preferences.
*/
private SharedPreferences preferences;
/**
* True if a new track should be created after the track recording service
* binds.
@@ -85,8 +90,6 @@ public class MyTracks extends TabActivity implements OnTouchListener {
*/
private GoogleAnalyticsTracker tracker;
private TrackRecordingServiceBinder serviceBinder;
/*
* Tabs/View navigation:
*/
@@ -99,23 +102,27 @@ public class MyTracks extends TabActivity implements OnTouchListener {
}
};
/*
* Recording service interaction:
*/
private final Runnable serviceBindCallback = new Runnable() {
@Override
public void run() {
synchronized (serviceBinder) {
ITrackRecordingService service = serviceBinder.getServiceIfBound();
synchronized (serviceConnection) {
ITrackRecordingService service = serviceConnection.getServiceIfBound();
if (startNewTrackRequested && service != null) {
Log.i(TAG, "Starting recording");
startNewTrackRequested = false;
startRecordingNewTrack(service);
} else {
} else if (startNewTrackRequested) {
Log.w(TAG, "Not yet starting recording");
}
}
}
};
private SharedPreferences preferences;
private TrackRecordingServiceConnection serviceConnection;
/*
* Application lifetime events:
@@ -142,7 +149,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
dataHub = TrackDataHub.newInstance(this);
menuManager = new MenuManager(this);
serviceBinder = TrackRecordingServiceBinder.getInstance(this);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
@@ -186,6 +193,45 @@ public class MyTracks extends TabActivity implements OnTouchListener {
Eula.showEulaRequireAcceptance(this);
}
@Override
protected void onStart() {
Log.d(TAG, "MyTracks.onStart");
super.onStart();
dataHub.start();
// Ensure that service is running and bound if we're supposed to be recording
if (ServiceUtils.isRecording(this, null, preferences)) {
serviceConnection.startAndBind();
}
Intent intent = getIntent();
String action = intent.getAction();
Uri data = intent.getData();
if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action))
&& TracksColumns.CONTENT_ITEMTYPE.equals(intent.getType())
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
long trackId = ContentUris.parseId(data);
dataHub.loadTrack(trackId);
}
}
@Override
protected void onResume() {
// Called when the current activity is being displayed or re-displayed
// to the user.
Log.d(TAG, "MyTracks.onResume");
serviceConnection.bindIfRunning();
super.onResume();
}
@Override
protected void onPause() {
// Called when activity is going into the background, but has not (yet) been
// killed. Shouldn't block longer than approx. 2 seconds.
Log.d(TAG, "MyTracks.onPause");
super.onPause();
}
@Override
protected void onStop() {
Log.d(TAG, "MyTracks.onStop");
@@ -201,50 +247,13 @@ public class MyTracks extends TabActivity implements OnTouchListener {
}
@Override
protected void onPause() {
// Called when activity is going into the background, but has not (yet) been
// killed. Shouldn't block longer than approx. 2 seconds.
Log.d(TAG, "MyTracks.onPause");
serviceBinder.unbindService();
super.onPause();
protected void onDestroy() {
Log.d(TAG, "MyTracks.onDestroy");
serviceConnection.unbind();
super.onDestroy();
}
@Override
protected void onResume() {
// Called when the current activity is being displayed or re-displayed
// to the user.
Log.d(TAG, "MyTracks.onResume");
serviceBinder.bindService(serviceBindCallback);
super.onResume();
}
@Override
protected void onStart() {
Log.d(TAG, "MyTracks.onStart");
super.onStart();
dataHub.start();
// Ensure that service is running if we're supposed to be recording
if (ServiceUtils.isRecording(this, preferences)) {
serviceBinder.startService();
}
Intent intent = getIntent();
String action = intent.getAction();
Uri data = intent.getData();
if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action))
&& TracksColumns.CONTENT_ITEMTYPE.equals(intent.getType())
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
long trackId = ContentUris.parseId(data);
dataHub.loadTrack(trackId);
}
}
/*
* Menu events:
* ============
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
@@ -254,7 +263,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null,
ServiceUtils.isRecording(this, preferences),
ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences),
dataHub.isATrackSelected());
return super.onPrepareOptionsMenu(menu);
}
@@ -273,8 +282,8 @@ public class MyTracks extends TabActivity implements OnTouchListener {
@Override
public boolean onTrackballEvent(MotionEvent event) {
if (ServiceUtils.isRecording(this, preferences)) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences)) {
try {
insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} catch (RemoteException e) {
@@ -285,6 +294,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
return true;
}
}
return super.onTrackballEvent(event);
}
@@ -402,7 +412,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
* @throws RemoteException If the call on the service failed.
*/
private long insertWaypoint(WaypointCreationRequest request) throws RemoteException {
ITrackRecordingService trackRecordingService = serviceBinder.getServiceIfBound();
ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService == null) {
throw new IllegalStateException("The recording service is not bound.");
}
@@ -443,9 +453,9 @@ public class MyTracks extends TabActivity implements OnTouchListener {
* it. Starts recording a new track.
*/
void startRecording() {
synchronized (serviceBinder) {
synchronized (serviceConnection) {
startNewTrackRequested = true;
serviceBinder.startService();
serviceConnection.startAndBind();
// Binding was already requested before, it either already happened
// (in which case running the callback manually triggers the actual recording start)
@@ -459,30 +469,29 @@ public class MyTracks extends TabActivity implements OnTouchListener {
* Stops the track recording service and unbinds from it. Will display a toast
* "Stopped recording" and pop up the Track Details activity.
*/
public void stopRecording() {
ITrackRecordingService trackRecordingService = serviceBinder.getServiceIfBound();
void stopRecording() {
// Save the track id as the shared preference will overwrite the recording track id.
SharedPreferences sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
long currentTrackId = sharedPreferences.getLong(getString(R.string.recording_track_key), -1);
ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService != null) {
// Save the track id as the shared preference will overwrite the recording track id.
SharedPreferences sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
long currentTrackId = sharedPreferences.getLong(getString(R.string.recording_track_key), -1);
try {
trackRecordingService.endCurrentTrack();
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Log.e(TAG, "Unable to stop recording.", e);
}
if (currentTrackId > 0) {
Intent intent = new Intent(MyTracks.this, TrackDetails.class);
intent.putExtra("trackid", currentTrackId);
intent.putExtra("hasCancelButton", false);
startActivity(intent);
}
}
serviceBinder.stopService();
}
serviceConnection.stop();
if (currentTrackId > 0) {
Intent intent = new Intent(MyTracks.this, TrackDetails.class);
intent.putExtra("trackid", currentTrackId);
intent.putExtra("hasCancelButton", false);
startActivity(intent);
}
}
void clearSelectedTrack() {
dataHub.unloadCurrentTrack();
@@ -16,9 +16,10 @@
package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceBinder;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.sensors.SensorUtils;
import com.google.android.maps.mytracks.R;
import com.google.protobuf.InvalidProtocolBufferException;
@@ -46,7 +47,6 @@ public class SensorStateActivity extends Activity {
private static final long REFRESH_PERIOD_MS = 250;
private final StatsUtilities utils;
private TrackRecordingServiceBinder serviceBinder;
/**
* This timer periodically invokes the refresh timer task.
@@ -59,6 +59,11 @@ public class SensorStateActivity extends Activity {
}
};
/**
* Connection to the recording service.
*/
private TrackRecordingServiceConnection serviceConnection;
/**
* A task which will update the U/I.
*/
@@ -81,39 +86,45 @@ public class SensorStateActivity extends Activity {
setContentView(R.layout.sensor_state);
serviceBinder = TrackRecordingServiceBinder.getInstance(this);
serviceBinder.bindService(stateUpdater);
// TODO: Allow reading from sensors even if service is not running.
serviceConnection = new TrackRecordingServiceConnection(this, stateUpdater);
serviceConnection.bindIfRunning();
updateState();
}
@Override
protected void onResume() {
super.onResume();
serviceConnection.bindIfRunning();
timer = new Timer();
timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS);
}
@Override
protected void onPause() {
super.onPause();
timer.cancel();
timer.purge();
timer = null;
super.onPause();
}
@Override
protected void onDestroy() {
serviceBinder.unbindService();
serviceConnection.unbind();
super.onDestroy();
}
protected void updateState() {
ITrackRecordingService service = serviceBinder.getServiceIfBound();
ITrackRecordingService service = serviceConnection.getServiceIfBound();
if (service == null) {
Log.d(Constants.TAG, "Could not get track recording service.");
updateSensorState(Sensor.SensorState.NONE);
updateSensorData(null);
return;
}
Sensor.SensorDataSet sds = null;
try {
byte[] buff = service.getSensorData();
@@ -159,8 +170,8 @@ public class SensorStateActivity extends Activity {
if (sds.hasPower() && sds.getPower().hasValue()
&& sds.getPower().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.power_state_register,
Integer.toString(sds.getPower().getValue()));
utils.setText(R.id.power_state_register,
Integer.toString(sds.getPower().getValue()));
} else {
utils.setText(R.id.power_state_register,
SensorUtils.getStateAsString(
@@ -186,14 +197,14 @@ public class SensorStateActivity extends Activity {
if (sds.hasHeartRate() && sds.getHeartRate().hasValue()
&& sds.getHeartRate().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.heart_rate_register,
Integer.toString(sds.getHeartRate().getValue()));
Integer.toString(sds.getHeartRate().getValue()));
} else {
utils.setText(R.id.heart_rate_register,
SensorUtils.getStateAsString(
sds.hasHeartRate()
? sds.getHeartRate().getState()
: Sensor.SensorState.NONE,
this));
utils.setText(R.id.heart_rate_register,
SensorUtils.getStateAsString(
sds.hasHeartRate()
? sds.getHeartRate().getState()
: Sensor.SensorState.NONE,
this));
}
}
}
@@ -90,7 +90,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
@Override
public void run() {
Log.i(TAG, "Started UI update thread");
while (ServiceUtils.isRecording(StatsActivity.this, preferences)) {
while (ServiceUtils.isRecording(StatsActivity.this, null, preferences)) {
runOnUiThread(updateResults);
try {
Thread.sleep(1000L);
@@ -1,12 +1,12 @@
/*
* Copyright 2008 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
@@ -19,6 +19,7 @@ import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
@@ -79,8 +80,7 @@ public class TrackList extends ListActivity
R.string.tracklist_show_track);
menu.add(0, Constants.MENU_EDIT, 0,
R.string.tracklist_edit_track);
if (!ServiceUtils.isRecording(TrackList.this, null)
|| trackId != recordingTrackId) {
if (!isRecording() || trackId != recordingTrackId) {
menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0,
@@ -112,6 +112,16 @@ public class TrackList extends ListActivity
}
};
private final Runnable serviceBindingChanged = new Runnable() {
@Override
public void run() {
updateButtonsEnabled();
}
};
private TrackRecordingServiceConnection serviceConnection;
private SharedPreferences preferences;
@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
@@ -214,16 +224,17 @@ public class TrackList extends ListActivity
listView = getListView();
listView.setOnCreateContextMenuListener(contextMenuListener);
SharedPreferences preferences =
getSharedPreferences(Constants.SETTINGS_NAME, 0);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindingChanged);
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
boolean notRecording = !ServiceUtils.isRecording(this, preferences);
deleteAll.setOnClickListener(this);
deleteAll.setEnabled(notRecording);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
exportAll.setOnClickListener(this);
exportAll.setEnabled(notRecording);
updateButtonsEnabled();
findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this);
preferences.registerOnSharedPreferenceChangeListener(this);
@@ -238,6 +249,29 @@ public class TrackList extends ListActivity
setListAdapter();
}
@Override
protected void onStart() {
super.onStart();
serviceConnection.bindIfRunning();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
private void updateButtonsEnabled() {
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
boolean notRecording = !isRecording();
deleteAll.setEnabled(notRecording);
exportAll.setEnabled(notRecording);
}
private void setListAdapter() {
// Get a cursor with all tracks
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
@@ -301,4 +335,8 @@ public class TrackList extends ListActivity
});
setListAdapter(adapter);
}
private boolean isRecording() {
return ServiceUtils.isRecording(TrackList.this, serviceConnection.getServiceIfBound(), preferences);
}
}
@@ -15,12 +15,14 @@
*/
package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceBinder;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
@@ -63,6 +65,7 @@ public class WaypointsList extends ListActivity
private Button insertStatisticsButton = null;
private long recordingTrackId = -1;
private MyTracksProviderUtils providerUtils;
private TrackRecordingServiceConnection serviceConnection;
private Cursor waypointsCursor = null;
@@ -128,6 +131,7 @@ public class WaypointsList extends ListActivity
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
serviceConnection = new TrackRecordingServiceConnection(this, null);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -174,6 +178,20 @@ public class WaypointsList extends ListActivity
setListAdapter();
}
@Override
protected void onResume() {
super.onResume();
serviceConnection.bindIfRunning();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
@Override
public void onClick(View v) {
WaypointCreationRequest request;
@@ -201,8 +219,7 @@ public class WaypointsList extends ListActivity
private long insertWaypoint(WaypointCreationRequest request) {
try {
ITrackRecordingService trackRecordingService =
TrackRecordingServiceBinder.getInstance(this).getServiceIfBound();
ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService != null) {
long waypointId = trackRecordingService.insertWaypoint(request);
if (waypointId >= 0) {
@@ -210,6 +227,8 @@ public class WaypointsList extends ListActivity
Toast.LENGTH_LONG).show();
return waypointId;
}
} else {
Log.e(TAG, "Not connected to service, not inserting waypoint");
}
} catch (RemoteException e) {
Log.e(Constants.TAG, "Cannot insert marker.", e);
@@ -20,11 +20,16 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.maps.mytracks.R;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.RemoteException;
import android.util.Log;
import java.util.List;
/**
* Helper for reading service state.
*
@@ -32,9 +37,17 @@ import android.util.Log;
*/
public class ServiceUtils {
public static boolean isRecording(Context ctx, SharedPreferences preferences) {
TrackRecordingServiceBinder serviceBinder = TrackRecordingServiceBinder.getInstance(ctx);
ITrackRecordingService service = serviceBinder.getServiceIfBound();
/**
* Checks whether we're currently recording.
* The checking is done by calling the service, if provided, or alternatively by reading
* recording state saved to preferences.
*
* @param ctx the current context
* @param service the service, or null if not bound to it
* @param preferences the preferences, or null if not available
* @return true if the service is recording (or supposed to be recording), false otherwise
*/
public static boolean isRecording(Context ctx, ITrackRecordingService service, SharedPreferences preferences) {
if (service != null) {
try {
return service.isRecording();
@@ -51,5 +64,25 @@ public class ServiceUtils {
return preferences.getLong(ctx.getString(R.string.recording_track_key), -1) > 0;
}
/**
* Checks whether the recording service is currently running.
*
* @param ctx the current context
* @return true if the service is running, false otherwise
*/
public static boolean isServiceRunning(Context ctx) {
ActivityManager activityManager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (RunningServiceInfo serviceInfo : services) {
ComponentName componentName = serviceInfo.service;
String serviceName = componentName.getClassName();
if (serviceName.equals(TrackRecordingService.class.getName())) {
return true;
}
}
return false;
}
private ServiceUtils() {}
}
@@ -1116,17 +1116,50 @@ public class TrackRecordingService extends Service {
*/
private static class ServiceBinder extends ITrackRecordingService.Stub {
private TrackRecordingService service;
private DeathRecipient deathRecipient;
public ServiceBinder(TrackRecordingService service) {
this.service = service;
}
// Logic for letting the actual service go up and down.
@Override
public boolean isBinderAlive() {
// Pretend dead if the service went down.
return service != null;
}
@Override
public boolean pingBinder() {
return isBinderAlive();
}
@Override
public void linkToDeath(DeathRecipient recipient, int flags) {
deathRecipient = recipient;
}
@Override
public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
if (!isBinderAlive()) {
return false;
}
deathRecipient = null;
return true;
}
/**
* Clears the reference to the outer class to minimize the leak.
*/
private void detachFromService() {
this.service = null;
attachInterface(null, null);
if (deathRecipient != null) {
deathRecipient.binderDied();
}
}
private void checkService() {
@@ -1,202 +0,0 @@
/*
* Copyright 2011 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.services;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.SystemUtils;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
import java.util.WeakHashMap;
/**
* A manager for the binding to the track recording service.
* This uses reference counting so multiple callers can share a binding.
*
* @author Rodrigo Damazio
*/
public class TrackRecordingServiceBinder {
/** Singleton instance. */
private static TrackRecordingServiceBinder instance;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(Constants.TAG, "Service now connected.");
// Delay setting the service until we are done with initialization.
ITrackRecordingService trackRecordingService =
ITrackRecordingService.Stub.asInterface(service);
synchronized (TrackRecordingServiceBinder.this) {
TrackRecordingServiceBinder.this.trackRecordingService = trackRecordingService;
try {
for (Runnable callback : pendingBindCallbacks.keySet()) {
if (callback != null) {
callback.run();
}
}
} finally {
pendingBindCallbacks.clear();
}
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.d(TAG, "Service now disconnected.");
synchronized (TrackRecordingServiceBinder.this) {
trackRecordingService = null;
}
}
};
/** Reference to the service, if bound. */
private ITrackRecordingService trackRecordingService;
/** Count of bindings to the service. */
private int bindCount = 0;
/** Set of callbacks to execute when the service connects. */
private final WeakHashMap<Runnable, Object> pendingBindCallbacks =
new WeakHashMap<Runnable, Object>();
/** Application context. */
private final Context applicationContext;
/**
* Starts the service with the default intent.
*
* This should only be called when the service will be put in recording mode,
* as the service being started means it'll be listening to the GPS.
* Notice that it is perfectly fine to call {@link #bindService} without calling
* this at all.
*/
public void startService() {
Intent intent = new Intent(applicationContext, TrackRecordingService.class);
applicationContext.startService(intent);
}
/**
* Binds to the service, and calls the given callback afterwards.
* Calls to this method should be balanced with calls to {@link #unbindService}.
*
* @param onBindCallback the callback for when the service is connected
*/
public void bindService(Runnable onBindCallback) {
synchronized (this) {
bindCount++;
if (trackRecordingService != null) {
if (onBindCallback != null) {
onBindCallback.run();
}
return;
}
if (onBindCallback != null) {
pendingBindCallbacks.put(onBindCallback, null);
}
if (bindCount == 1) {
Intent intent = new Intent(applicationContext, TrackRecordingService.class);
int flags = SystemUtils.isRelease(applicationContext) ? 0 : Context.BIND_DEBUG_UNBIND;
applicationContext.bindService(intent, serviceConnection, flags);
}
}
}
/**
* Unbinds from the service.
*
* This will unbind from the service, unless another caller is still bound to it.
* Calls to this method should be balanced with calls to {@link #bindService}.
*/
public void unbindService() {
synchronized (this) {
bindCount--;
if (bindCount > 0) {
// Someone else may still be using it.
return;
} else if (bindCount < 0) {
Log.e(TAG, "Unbalanced binding calls.");
return;
}
try {
applicationContext.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
Log.d(TAG, "Tried unbinding, but service was not registered.", e);
}
}
}
/**
* Stops the service, and consequently its GPS listening.
*/
public void stopService() {
try {
applicationContext.stopService(new Intent(applicationContext, TrackRecordingService.class));
} catch (SecurityException e) {
Log.e(TAG, "Encountered a security exception when trying to stop service.", e);
}
}
@Override
protected void finalize() throws Throwable {
if (bindCount > 0) {
Log.e(TAG, "Leaked bindings: " + bindCount + " bindings left on finalize.");
}
super.finalize();
}
/**
* Returns the connected service instance, or null if not connected.
*/
public ITrackRecordingService getServiceIfBound() {
return trackRecordingService;
}
/**
* Returns the singleton instance of this class.
* Notice that although a context is required, it is safe to call this from
* multiple different contexts to obtain the same instance.
*
* @param context the current context
* @return the singleton instance
*/
public synchronized static TrackRecordingServiceBinder getInstance(Context context) {
if (instance == null) {
instance = new TrackRecordingServiceBinder(context.getApplicationContext());
}
return instance;
}
/**
* Internal constructor.
*
* @param applicationContext the current application (not activity) context
*/
private TrackRecordingServiceBinder(Context applicationContext) {
this.applicationContext = applicationContext;
}
}
@@ -0,0 +1,117 @@
package com.google.android.apps.mytracks.services;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.util.SystemUtils;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.util.Log;
public class TrackRecordingServiceConnection {
private ITrackRecordingService boundService;
private final DeathRecipient deathRecipient = new DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "Service died");
setBoundService(null);
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to service");
try {
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient", e);
}
setBoundService(ITrackRecordingService.Stub.asInterface(service));
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "Disconnected from service");
setBoundService(null);
}
};
private final Context context;
private final Runnable bindChangedCallback;
public TrackRecordingServiceConnection(Context context, Runnable bindChangedCallback) {
this.context = context;
this.bindChangedCallback = bindChangedCallback;
}
public void startAndBind() {
bindService(true);
}
public void bindIfRunning() {
bindService(false);
}
public void stop() {
unbind();
Log.d(TAG, "Stopping service");
Intent intent = new Intent(context, TrackRecordingService.class);
context.stopService(intent);
}
public void unbind() {
Log.d(TAG, "Unbinding from the service");
try {
context.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// Means we weren't bound, which is ok.
}
setBoundService(null);
}
public ITrackRecordingService getServiceIfBound() {
return boundService;
}
private void bindService(boolean startIfNeeded) {
if (boundService != null) {
// Already bound.
return;
}
if (!startIfNeeded && !ServiceUtils.isServiceRunning(context)) {
// Not running, start not requested.
Log.d(TAG, "Service not running, not binding to it.");
return;
}
if (startIfNeeded) {
Log.i(TAG, "Starting the service");
Intent intent = new Intent(context, TrackRecordingService.class);
context.startService(intent);
}
Log.i(TAG, "Binding to the service");
Intent intent = new Intent(context, TrackRecordingService.class);
int flags = SystemUtils.isRelease(context) ? 0 : Context.BIND_DEBUG_UNBIND;
context.bindService(intent, serviceConnection, flags);
}
private void setBoundService(ITrackRecordingService service) {
boundService = service;
if (bindChangedCallback != null) {
bindChangedCallback.run();
}
}
}
@@ -1,12 +1,12 @@
/*
* 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
@@ -16,6 +16,7 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -33,11 +34,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* A unit test for {@link MyTracks} activity.
*
*
* @author Bartlomiej Niechwiej
*/
public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection serviceConnection;
public MyTracksTest() {
super(MyTracks.class);
@@ -126,7 +128,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
assertInitialized();
// Check if not recording.
clearSelectedAndRecordingTracks();
clearSelectedAndRecordingTracks();
waitForIdle();
assertFalse(isRecording());
@@ -137,6 +139,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
// Start a new track.
getActivity().startRecording();
serviceConnection.bindIfRunning();
long recordingTrackId = awaitRecordingStatus(5000, true);
assertTrue(recordingTrackId >= 0);
@@ -149,7 +152,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
assertEquals(recordingTrackId, selectedTrackId);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
// Watch for MyTracksDetails activity.
// Watch for MyTracksDetails activity.
ActivityMonitor monitor = getInstrumentation().addMonitor(
TrackDetails.class.getName(), null, false);
@@ -157,7 +160,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
// no longer recording.
getActivity().stopRecording();
// Check if we got back MyTracksDetails activity.
// Check if we got back MyTracksDetails activity.
Activity activity = getInstrumentation().waitForMonitor(monitor);
assertTrue(activity instanceof TrackDetails);
@@ -180,13 +183,15 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
private void assertInitialized() {
assertNotNull(getActivity());
serviceConnection = new TrackRecordingServiceConnection(getActivity(), null);
}
/**
* Waits until the UI thread becomes idle.
*/
private void waitForIdle() throws InterruptedException {
// Note: We can't use getInstrumentation().waitForIdleSync() here.
// Note: We can't use getInstrumentation().waitForIdleSync() here.
final Object semaphore = new Object();
synchronized (semaphore) {
final AtomicBoolean isIdle = new AtomicBoolean();
@@ -216,10 +221,10 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
editor.clear();
editor.commit();
}
/**
* Waits until the recording state changes to the given status.
*
*
* @param timeout the maximum time to wait, in milliseconds.
* @param isRecording the final status to await.
* @return the recording track ID.
@@ -250,6 +255,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
}
private boolean isRecording() {
return ServiceUtils.isRecording(getActivity(), getSharedPreferences());
return ServiceUtils.isRecording(getActivity(),
serviceConnection.getServiceIfBound(), getSharedPreferences());
}
}
@@ -49,9 +49,9 @@ import java.util.List;
/**
* Tests for the MyTracks track recording service.
*
*
* @author Bartlomiej Niechwiej
*
*
* TODO: The original class, ServiceTestCase, has a few limitations, e.g.
* it's not possible to properly shutdown the service, unless tearDown()
* is called, which prevents from testing multiple scenarios in a single
@@ -63,7 +63,7 @@ public class TrackRecordingServiceTest
private Context context;
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
/*
* In order to support starting and binding to the service in the same
* unit test, we provide a workaround, as the original class doesn't allow
@@ -72,19 +72,19 @@ public class TrackRecordingServiceTest
private boolean bound;
private Intent serviceIntent;
public TrackRecordingServiceTest() {
super(TrackRecordingService.class);
}
/**
* A context wrapper with the user provided {@link ContentResolver}.
*
*
* TODO: Move to test utils package.
*/
public static class MockContext extends ContextWrapper {
private final ContentResolver contentResolver;
public MockContext(ContentResolver contentResolver, Context base) {
super(base);
this.contentResolver = contentResolver;
@@ -108,7 +108,7 @@ public class TrackRecordingServiceTest
return 4;
}
}
@Override
protected IBinder bindService(Intent intent) {
if (getService() != null) {
@@ -190,7 +190,7 @@ public class TrackRecordingServiceTest
startService(startIntent);
assertNotNull(getService());
// We expect to resume the previous track.
// We expect to resume the previous track.
assertTrue(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(123, service.getRecordingTrackId());
@@ -293,7 +293,7 @@ public class TrackRecordingServiceTest
assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent());
// Test if we start in no-recording mode by default.
// Test if we start in no-recording mode by default.
assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId());
}
@@ -463,92 +463,19 @@ public class TrackRecordingServiceTest
context.getString(R.string.recording_track_key), 0));
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testIntegration_completeRecordingSession() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty());
fullRecordingSession();
}
@MediumTest
public void testDeleteAllTracks_noRecording() throws Exception {
createDummyTrack(123, -1, false);
assertEquals(1, providerUtils.getAllTracks().size());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// Deleting all tracks should succeed.
service.deleteAllTracks();
assertFalse(service.isRecording());
assertTrue(providerUtils.getAllTracks().isEmpty());
}
@MediumTest
public void testDeleteAllTracks_noTracks() throws Exception {
assertTrue(providerUtils.getAllTracks().isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// Deleting all tracks should succeed.
service.deleteAllTracks();
assertFalse(service.isRecording());
assertTrue(providerUtils.getAllTracks().isEmpty());
}
@MediumTest
public void testDeleteAllTracks_trackInProgress() throws Exception {
createDummyTrack(123, -1, true);
assertEquals(1, providerUtils.getAllTracks().size());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
// Since we have a track in progress, we expect to fail.
try {
service.deleteAllTracks();
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
assertTrue(service.isRecording());
assertEquals(1, providerUtils.getAllTracks().size());
}
@MediumTest
public void testHasRecorded_noTracks() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
assertFalse(service.hasRecorded());
}
@MediumTest
public void testHasRecorded_trackInProgress() throws Exception {
createDummyTrack(123, -1, true);
assertEquals(1, providerUtils.getAllTracks().size());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
assertTrue(service.hasRecorded());
}
@MediumTest
public void testHasRecorded_oldTracks() throws Exception {
createDummyTrack(123, -1, false);
assertEquals(1, providerUtils.getAllTracks().size());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
assertTrue(service.hasRecorded());
}
@MediumTest
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
try {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
fail("Expecting IllegalStateException");
@@ -556,17 +483,17 @@ public class TrackRecordingServiceTest
// Expected.
}
}
@MediumTest
public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.stats_icon_url),
wpt.getIcon());
@@ -586,7 +513,7 @@ public class TrackRecordingServiceTest
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
try {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER);
fail("Expecting IllegalStateException");
@@ -594,14 +521,14 @@ public class TrackRecordingServiceTest
// Expected.
}
}
@MediumTest
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER));
Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.waypoint_icon_url),
@@ -614,7 +541,7 @@ public class TrackRecordingServiceTest
assertNotNull(wpt.getLocation());
assertNull(wpt.getStatistics());
}
@MediumTest
public void testWithProperties_noAnnouncementFreq() throws Exception {
functionalTest(R.string.announcement_frequency_key, (Object) null);
@@ -624,7 +551,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_defaultAnnouncementFreq() throws Exception {
functionalTest(R.string.announcement_frequency_key, 1);
}
@MediumTest
public void testWithProperties_noMaxRecordingDist() throws Exception {
functionalTest(R.string.max_recording_distance_key, (Object) null);
@@ -654,7 +581,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_defaultSplitFreqByDist() throws Exception {
functionalTest(R.string.split_frequency_key, 5);
}
@MediumTest
public void testWithProperties_defaultSplitFreqByTime() throws Exception {
functionalTest(R.string.split_frequency_key, -2);
@@ -674,7 +601,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_metricUnitsDisabled() throws Exception {
functionalTest(R.string.metric_units_key, false);
}
@MediumTest
public void testWithProperties_noMinRecordingInterval() throws Exception {
functionalTest(R.string.min_recording_interval_key, (Object) null);
@@ -690,23 +617,23 @@ public class TrackRecordingServiceTest
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);
}
@MediumTest
public void testWithProperties_noSensorType() throws Exception {
functionalTest(R.string.sensor_type_key, (Object) null);
}
@MediumTest
public void testWithProperties_zephyrSensorType() throws Exception {
functionalTest(R.string.sensor_type_key,
context.getString(R.string.zephyr_sensor_type));
context.getString(R.string.zephyr_sensor_type));
}
private ITrackRecordingService bindAndGetService(Intent intent) {
ITrackRecordingService service = ITrackRecordingService.Stub.asInterface(
bindService(intent));
@@ -724,7 +651,7 @@ public class TrackRecordingServiceTest
addTrack(dummyTrack, isRecording);
return dummyTrack;
}
private void updateAutoResumePrefs(int attempts, int timeoutMins) {
Editor editor = sharedPreferences.edit();
editor.putInt(context.getString(
@@ -733,20 +660,20 @@ public class TrackRecordingServiceTest
R.string.auto_resume_track_timeout_key), timeoutMins);
editor.commit();
}
private Intent createStartIntent() {
Intent startIntent = new Intent();
startIntent.setClass(context, TrackRecordingService.class);
return startIntent;
}
private void addTrack(Track track, boolean isRecording) {
assertTrue(track.getId() >= 0);
providerUtils.insertTrack(track);
assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
setRecordingTrack(isRecording ? track.getId() : -1);
}
private void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(context.getString(R.string.recording_track_key), id);
@@ -760,11 +687,11 @@ public class TrackRecordingServiceTest
throws Exception {
final String key = context.getString(resourceId);
for (Object value : values) {
// Remove all properties and set the property for the given key.
// Remove all properties and set the property for the given key.
Editor editor = sharedPreferences.edit();
editor.clear();
if (value instanceof String) {
editor.putString(key, (String) value);
editor.putString(key, (String) value);
} else if (value instanceof Long) {
editor.putLong(key, (Long) value);
} else if (value instanceof Integer) {
@@ -775,15 +702,15 @@ public class TrackRecordingServiceTest
// Do nothing, as clear above has already removed this property.
}
editor.commit();
fullRecordingSession();
}
}
private void fullRecordingSession() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// Start a track.
long id = service.startNewTrack();
assertTrue(id >= 0);
@@ -806,14 +733,14 @@ public class TrackRecordingServiceTest
loc.setTime(startTime + i * 10000);
loc.setBearing(3.0f);
service.recordLocation(loc);
if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER);
}
}
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
assertFalse(service.isRecording());