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.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils; 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.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.SystemUtils;
@@ -64,11 +64,16 @@ import android.widget.Toast;
public class MyTracks extends TabActivity implements OnTouchListener { public class MyTracks extends TabActivity implements OnTouchListener {
private TrackDataHub dataHub; private TrackDataHub dataHub;
/* /**
* Menu manager. * Menu manager.
*/ */
private MenuManager menuManager; private MenuManager menuManager;
/**
* Preferences.
*/
private SharedPreferences preferences;
/** /**
* True if a new track should be created after the track recording service * True if a new track should be created after the track recording service
* binds. * binds.
@@ -85,8 +90,6 @@ public class MyTracks extends TabActivity implements OnTouchListener {
*/ */
private GoogleAnalyticsTracker tracker; private GoogleAnalyticsTracker tracker;
private TrackRecordingServiceBinder serviceBinder;
/* /*
* Tabs/View navigation: * Tabs/View navigation:
*/ */
@@ -99,23 +102,27 @@ public class MyTracks extends TabActivity implements OnTouchListener {
} }
}; };
/*
* Recording service interaction:
*/
private final Runnable serviceBindCallback = new Runnable() { private final Runnable serviceBindCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
synchronized (serviceBinder) { synchronized (serviceConnection) {
ITrackRecordingService service = serviceBinder.getServiceIfBound(); ITrackRecordingService service = serviceConnection.getServiceIfBound();
if (startNewTrackRequested && service != null) { if (startNewTrackRequested && service != null) {
Log.i(TAG, "Starting recording"); Log.i(TAG, "Starting recording");
startNewTrackRequested = false; startNewTrackRequested = false;
startRecordingNewTrack(service); startRecordingNewTrack(service);
} else { } else if (startNewTrackRequested) {
Log.w(TAG, "Not yet starting recording"); Log.w(TAG, "Not yet starting recording");
} }
} }
} }
}; };
private SharedPreferences preferences; private TrackRecordingServiceConnection serviceConnection;
/* /*
* Application lifetime events: * Application lifetime events:
@@ -142,7 +149,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
dataHub = TrackDataHub.newInstance(this); dataHub = TrackDataHub.newInstance(this);
menuManager = new MenuManager(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 // The volume we want to control is the Text-To-Speech volume
int volumeStream = int volumeStream =
@@ -186,6 +193,45 @@ public class MyTracks extends TabActivity implements OnTouchListener {
Eula.showEulaRequireAcceptance(this); 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 @Override
protected void onStop() { protected void onStop() {
Log.d(TAG, "MyTracks.onStop"); Log.d(TAG, "MyTracks.onStop");
@@ -201,50 +247,13 @@ public class MyTracks extends TabActivity implements OnTouchListener {
} }
@Override @Override
protected void onPause() { protected void onDestroy() {
// Called when activity is going into the background, but has not (yet) been Log.d(TAG, "MyTracks.onDestroy");
// killed. Shouldn't block longer than approx. 2 seconds. serviceConnection.unbind();
Log.d(TAG, "MyTracks.onPause");
serviceBinder.unbindService(); super.onDestroy();
super.onPause();
} }
@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 @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);
@@ -254,7 +263,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null, menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null,
ServiceUtils.isRecording(this, preferences), ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences),
dataHub.isATrackSelected()); dataHub.isATrackSelected());
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
} }
@@ -273,8 +282,8 @@ public class MyTracks extends TabActivity implements OnTouchListener {
@Override @Override
public boolean onTrackballEvent(MotionEvent event) { 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 { try {
insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} catch (RemoteException e) { } catch (RemoteException e) {
@@ -285,6 +294,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
return true; return true;
} }
} }
return super.onTrackballEvent(event); return super.onTrackballEvent(event);
} }
@@ -402,7 +412,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
* @throws RemoteException If the call on the service failed. * @throws RemoteException If the call on the service failed.
*/ */
private long insertWaypoint(WaypointCreationRequest request) throws RemoteException { private long insertWaypoint(WaypointCreationRequest request) throws RemoteException {
ITrackRecordingService trackRecordingService = serviceBinder.getServiceIfBound(); ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService == null) { if (trackRecordingService == null) {
throw new IllegalStateException("The recording service is not bound."); 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. * it. Starts recording a new track.
*/ */
void startRecording() { void startRecording() {
synchronized (serviceBinder) { synchronized (serviceConnection) {
startNewTrackRequested = true; startNewTrackRequested = true;
serviceBinder.startService(); serviceConnection.startAndBind();
// Binding was already requested before, it either already happened // Binding was already requested before, it either already happened
// (in which case running the callback manually triggers the actual recording start) // (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 * Stops the track recording service and unbinds from it. Will display a toast
* "Stopped recording" and pop up the Track Details activity. * "Stopped recording" and pop up the Track Details activity.
*/ */
public void stopRecording() { void stopRecording() {
ITrackRecordingService trackRecordingService = serviceBinder.getServiceIfBound(); // 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) { 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 { try {
trackRecordingService.endCurrentTrack(); 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) { } catch (Exception e) {
Log.e(TAG, "Unable to stop recording.", 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() { void clearSelectedTrack() {
dataHub.unloadCurrentTrack(); dataHub.unloadCurrentTrack();
@@ -16,9 +16,10 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.services.ITrackRecordingService; 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.apps.mytracks.services.sensors.SensorUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
@@ -46,7 +47,6 @@ public class SensorStateActivity extends Activity {
private static final long REFRESH_PERIOD_MS = 250; private static final long REFRESH_PERIOD_MS = 250;
private final StatsUtilities utils; private final StatsUtilities utils;
private TrackRecordingServiceBinder serviceBinder;
/** /**
* This timer periodically invokes the refresh timer task. * 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. * A task which will update the U/I.
*/ */
@@ -81,39 +86,45 @@ public class SensorStateActivity extends Activity {
setContentView(R.layout.sensor_state); setContentView(R.layout.sensor_state);
serviceBinder = TrackRecordingServiceBinder.getInstance(this); // TODO: Allow reading from sensors even if service is not running.
serviceBinder.bindService(stateUpdater); serviceConnection = new TrackRecordingServiceConnection(this, stateUpdater);
serviceConnection.bindIfRunning();
updateState(); updateState();
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
serviceConnection.bindIfRunning();
timer = new Timer(); timer = new Timer();
timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS); timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS);
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause();
timer.cancel(); timer.cancel();
timer.purge(); timer.purge();
timer = null; timer = null;
super.onPause();
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
serviceBinder.unbindService(); serviceConnection.unbind();
super.onDestroy();
} }
protected void updateState() { protected void updateState() {
ITrackRecordingService service = serviceBinder.getServiceIfBound(); ITrackRecordingService service = serviceConnection.getServiceIfBound();
if (service == null) { if (service == null) {
Log.d(Constants.TAG, "Could not get track recording service."); Log.d(Constants.TAG, "Could not get track recording service.");
updateSensorState(Sensor.SensorState.NONE); updateSensorState(Sensor.SensorState.NONE);
updateSensorData(null); updateSensorData(null);
return; return;
} }
Sensor.SensorDataSet sds = null; Sensor.SensorDataSet sds = null;
try { try {
byte[] buff = service.getSensorData(); byte[] buff = service.getSensorData();
@@ -159,8 +170,8 @@ public class SensorStateActivity extends Activity {
if (sds.hasPower() && sds.getPower().hasValue() if (sds.hasPower() && sds.getPower().hasValue()
&& sds.getPower().getState() == Sensor.SensorState.SENDING) { && sds.getPower().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.power_state_register, utils.setText(R.id.power_state_register,
Integer.toString(sds.getPower().getValue())); Integer.toString(sds.getPower().getValue()));
} else { } else {
utils.setText(R.id.power_state_register, utils.setText(R.id.power_state_register,
SensorUtils.getStateAsString( SensorUtils.getStateAsString(
@@ -186,14 +197,14 @@ public class SensorStateActivity extends Activity {
if (sds.hasHeartRate() && sds.getHeartRate().hasValue() if (sds.hasHeartRate() && sds.getHeartRate().hasValue()
&& sds.getHeartRate().getState() == Sensor.SensorState.SENDING) { && sds.getHeartRate().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.heart_rate_register, utils.setText(R.id.heart_rate_register,
Integer.toString(sds.getHeartRate().getValue())); Integer.toString(sds.getHeartRate().getValue()));
} else { } else {
utils.setText(R.id.heart_rate_register, utils.setText(R.id.heart_rate_register,
SensorUtils.getStateAsString( SensorUtils.getStateAsString(
sds.hasHeartRate() sds.hasHeartRate()
? sds.getHeartRate().getState() ? sds.getHeartRate().getState()
: Sensor.SensorState.NONE, : Sensor.SensorState.NONE,
this)); this));
} }
} }
} }
@@ -90,7 +90,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
@Override @Override
public void run() { public void run() {
Log.i(TAG, "Started UI update thread"); Log.i(TAG, "Started UI update thread");
while (ServiceUtils.isRecording(StatsActivity.this, preferences)) { while (ServiceUtils.isRecording(StatsActivity.this, null, preferences)) {
runOnUiThread(updateResults); runOnUiThread(updateResults);
try { try {
Thread.sleep(1000L); Thread.sleep(1000L);
@@ -1,12 +1,12 @@
/* /*
* Copyright 2008 Google Inc. * Copyright 2008 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * 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.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity; import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity;
import com.google.android.apps.mytracks.services.ServiceUtils; 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.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -79,8 +80,7 @@ public class TrackList extends ListActivity
R.string.tracklist_show_track); R.string.tracklist_show_track);
menu.add(0, Constants.MENU_EDIT, 0, menu.add(0, Constants.MENU_EDIT, 0,
R.string.tracklist_edit_track); R.string.tracklist_edit_track);
if (!ServiceUtils.isRecording(TrackList.this, null) if (!isRecording() || trackId != recordingTrackId) {
|| trackId != recordingTrackId) {
menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0, menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google); R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0, 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 @Override
public void onSharedPreferenceChanged( public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) { SharedPreferences sharedPreferences, String key) {
@@ -214,16 +224,17 @@ public class TrackList extends ListActivity
listView = getListView(); listView = getListView();
listView.setOnCreateContextMenuListener(contextMenuListener); listView.setOnCreateContextMenuListener(contextMenuListener);
SharedPreferences preferences = preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
getSharedPreferences(Constants.SETTINGS_NAME, 0); serviceConnection = new TrackRecordingServiceConnection(this, serviceBindingChanged);
View deleteAll = findViewById(R.id.tracklist_btn_delete_all); 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.setOnClickListener(this);
deleteAll.setEnabled(notRecording);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
exportAll.setOnClickListener(this); exportAll.setOnClickListener(this);
exportAll.setEnabled(notRecording);
updateButtonsEnabled();
findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this); findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this);
preferences.registerOnSharedPreferenceChangeListener(this); preferences.registerOnSharedPreferenceChangeListener(this);
@@ -238,6 +249,29 @@ public class TrackList extends ListActivity
setListAdapter(); 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() { private void setListAdapter() {
// Get a cursor with all tracks // Get a cursor with all tracks
SimpleCursorAdapter adapter = new SimpleCursorAdapter( SimpleCursorAdapter adapter = new SimpleCursorAdapter(
@@ -301,4 +335,8 @@ public class TrackList extends ListActivity
}); });
setListAdapter(adapter); setListAdapter(adapter);
} }
private boolean isRecording() {
return ServiceUtils.isRecording(TrackList.this, serviceConnection.getServiceIfBound(), preferences);
}
} }
@@ -15,12 +15,14 @@
*/ */
package com.google.android.apps.mytracks; 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.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.ITrackRecordingService; 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.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -63,6 +65,7 @@ public class WaypointsList extends ListActivity
private Button insertStatisticsButton = null; private Button insertStatisticsButton = null;
private long recordingTrackId = -1; private long recordingTrackId = -1;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
private TrackRecordingServiceConnection serviceConnection;
private Cursor waypointsCursor = null; private Cursor waypointsCursor = null;
@@ -128,6 +131,7 @@ public class WaypointsList extends ListActivity
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this); providerUtils = MyTracksProviderUtils.Factory.get(this);
serviceConnection = new TrackRecordingServiceConnection(this, null);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -174,6 +178,20 @@ public class WaypointsList extends ListActivity
setListAdapter(); setListAdapter();
} }
@Override
protected void onResume() {
super.onResume();
serviceConnection.bindIfRunning();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
@Override @Override
public void onClick(View v) { public void onClick(View v) {
WaypointCreationRequest request; WaypointCreationRequest request;
@@ -201,8 +219,7 @@ public class WaypointsList extends ListActivity
private long insertWaypoint(WaypointCreationRequest request) { private long insertWaypoint(WaypointCreationRequest request) {
try { try {
ITrackRecordingService trackRecordingService = ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
TrackRecordingServiceBinder.getInstance(this).getServiceIfBound();
if (trackRecordingService != null) { if (trackRecordingService != null) {
long waypointId = trackRecordingService.insertWaypoint(request); long waypointId = trackRecordingService.insertWaypoint(request);
if (waypointId >= 0) { if (waypointId >= 0) {
@@ -210,6 +227,8 @@ public class WaypointsList extends ListActivity
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
return waypointId; return waypointId;
} }
} else {
Log.e(TAG, "Not connected to service, not inserting waypoint");
} }
} catch (RemoteException e) { } catch (RemoteException e) {
Log.e(Constants.TAG, "Cannot insert marker.", 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.apps.mytracks.Constants;
import com.google.android.maps.mytracks.R; 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.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.RemoteException; import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import java.util.List;
/** /**
* Helper for reading service state. * Helper for reading service state.
* *
@@ -32,9 +37,17 @@ import android.util.Log;
*/ */
public class ServiceUtils { public class ServiceUtils {
public static boolean isRecording(Context ctx, SharedPreferences preferences) { /**
TrackRecordingServiceBinder serviceBinder = TrackRecordingServiceBinder.getInstance(ctx); * Checks whether we're currently recording.
ITrackRecordingService service = serviceBinder.getServiceIfBound(); * 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) { if (service != null) {
try { try {
return service.isRecording(); return service.isRecording();
@@ -51,5 +64,25 @@ public class ServiceUtils {
return preferences.getLong(ctx.getString(R.string.recording_track_key), -1) > 0; 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() {} private ServiceUtils() {}
} }
@@ -1116,17 +1116,50 @@ public class TrackRecordingService extends Service {
*/ */
private static class ServiceBinder extends ITrackRecordingService.Stub { private static class ServiceBinder extends ITrackRecordingService.Stub {
private TrackRecordingService service; private TrackRecordingService service;
private DeathRecipient deathRecipient;
public ServiceBinder(TrackRecordingService service) { public ServiceBinder(TrackRecordingService service) {
this.service = 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. * Clears the reference to the outer class to minimize the leak.
*/ */
private void detachFromService() { private void detachFromService() {
this.service = null; this.service = null;
attachInterface(null, null); attachInterface(null, null);
if (deathRecipient != null) {
deathRecipient.binderDied();
}
} }
private void checkService() { 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. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -16,6 +16,7 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.services.ServiceUtils; import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
@@ -33,11 +34,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* A unit test for {@link MyTracks} activity. * A unit test for {@link MyTracks} activity.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
*/ */
public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection serviceConnection;
public MyTracksTest() { public MyTracksTest() {
super(MyTracks.class); super(MyTracks.class);
@@ -126,7 +128,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
assertInitialized(); assertInitialized();
// Check if not recording. // Check if not recording.
clearSelectedAndRecordingTracks(); clearSelectedAndRecordingTracks();
waitForIdle(); waitForIdle();
assertFalse(isRecording()); assertFalse(isRecording());
@@ -137,6 +139,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
// Start a new track. // Start a new track.
getActivity().startRecording(); getActivity().startRecording();
serviceConnection.bindIfRunning();
long recordingTrackId = awaitRecordingStatus(5000, true); long recordingTrackId = awaitRecordingStatus(5000, true);
assertTrue(recordingTrackId >= 0); assertTrue(recordingTrackId >= 0);
@@ -149,7 +152,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
assertEquals(recordingTrackId, selectedTrackId); assertEquals(recordingTrackId, selectedTrackId);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
// Watch for MyTracksDetails activity. // Watch for MyTracksDetails activity.
ActivityMonitor monitor = getInstrumentation().addMonitor( ActivityMonitor monitor = getInstrumentation().addMonitor(
TrackDetails.class.getName(), null, false); TrackDetails.class.getName(), null, false);
@@ -157,7 +160,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
// no longer recording. // no longer recording.
getActivity().stopRecording(); getActivity().stopRecording();
// Check if we got back MyTracksDetails activity. // Check if we got back MyTracksDetails activity.
Activity activity = getInstrumentation().waitForMonitor(monitor); Activity activity = getInstrumentation().waitForMonitor(monitor);
assertTrue(activity instanceof TrackDetails); assertTrue(activity instanceof TrackDetails);
@@ -180,13 +183,15 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
private void assertInitialized() { private void assertInitialized() {
assertNotNull(getActivity()); assertNotNull(getActivity());
serviceConnection = new TrackRecordingServiceConnection(getActivity(), null);
} }
/** /**
* Waits until the UI thread becomes idle. * Waits until the UI thread becomes idle.
*/ */
private void waitForIdle() throws InterruptedException { 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(); final Object semaphore = new Object();
synchronized (semaphore) { synchronized (semaphore) {
final AtomicBoolean isIdle = new AtomicBoolean(); final AtomicBoolean isIdle = new AtomicBoolean();
@@ -216,10 +221,10 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
editor.clear(); editor.clear();
editor.commit(); editor.commit();
} }
/** /**
* Waits until the recording state changes to the given status. * Waits until the recording state changes to the given status.
* *
* @param timeout the maximum time to wait, in milliseconds. * @param timeout the maximum time to wait, in milliseconds.
* @param isRecording the final status to await. * @param isRecording the final status to await.
* @return the recording track ID. * @return the recording track ID.
@@ -250,6 +255,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
} }
private boolean isRecording() { 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. * Tests for the MyTracks track recording service.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* *
* TODO: The original class, ServiceTestCase, has a few limitations, e.g. * TODO: The original class, ServiceTestCase, has a few limitations, e.g.
* it's not possible to properly shutdown the service, unless tearDown() * it's not possible to properly shutdown the service, unless tearDown()
* is called, which prevents from testing multiple scenarios in a single * is called, which prevents from testing multiple scenarios in a single
@@ -63,7 +63,7 @@ public class TrackRecordingServiceTest
private Context context; private Context context;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
/* /*
* In order to support starting and binding to the service in the same * 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 * unit test, we provide a workaround, as the original class doesn't allow
@@ -72,19 +72,19 @@ public class TrackRecordingServiceTest
private boolean bound; private boolean bound;
private Intent serviceIntent; private Intent serviceIntent;
public TrackRecordingServiceTest() { public TrackRecordingServiceTest() {
super(TrackRecordingService.class); super(TrackRecordingService.class);
} }
/** /**
* A context wrapper with the user provided {@link ContentResolver}. * A context wrapper with the user provided {@link ContentResolver}.
* *
* TODO: Move to test utils package. * TODO: Move to test utils package.
*/ */
public static class MockContext extends ContextWrapper { public static class MockContext extends ContextWrapper {
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
public MockContext(ContentResolver contentResolver, Context base) { public MockContext(ContentResolver contentResolver, Context base) {
super(base); super(base);
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
@@ -108,7 +108,7 @@ public class TrackRecordingServiceTest
return 4; return 4;
} }
} }
@Override @Override
protected IBinder bindService(Intent intent) { protected IBinder bindService(Intent intent) {
if (getService() != null) { if (getService() != null) {
@@ -190,7 +190,7 @@ public class TrackRecordingServiceTest
startService(startIntent); startService(startIntent);
assertNotNull(getService()); assertNotNull(getService());
// We expect to resume the previous track. // We expect to resume the previous track.
assertTrue(getService().isRecording()); assertTrue(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(123, service.getRecordingTrackId()); assertEquals(123, service.getRecordingTrackId());
@@ -293,7 +293,7 @@ public class TrackRecordingServiceTest
assertTrue(tracks.isEmpty()); assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent()); 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()); assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId()); assertEquals(-1, service.getRecordingTrackId());
} }
@@ -463,92 +463,19 @@ public class TrackRecordingServiceTest
context.getString(R.string.recording_track_key), 0)); context.getString(R.string.recording_track_key), 0));
assertEquals(-1, service.getRecordingTrackId()); assertEquals(-1, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
public void testIntegration_completeRecordingSession() throws Exception { public void testIntegration_completeRecordingSession() throws Exception {
List<Track> tracks = providerUtils.getAllTracks(); List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty()); assertTrue(tracks.isEmpty());
fullRecordingSession(); 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 @MediumTest
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception { public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
try { try {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
fail("Expecting IllegalStateException"); fail("Expecting IllegalStateException");
@@ -556,17 +483,17 @@ public class TrackRecordingServiceTest
// Expected. // Expected.
} }
} }
@MediumTest @MediumTest
public void testInsertStatisticsMarker_validLocation() throws Exception { public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(123, -1, true); createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
Waypoint wpt = providerUtils.getWaypoint(1); Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.stats_icon_url), assertEquals(getContext().getString(R.string.stats_icon_url),
wpt.getIcon()); wpt.getIcon());
@@ -586,7 +513,7 @@ public class TrackRecordingServiceTest
public void testInsertWaypointMarker_noRecordingTrack() throws Exception { public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
try { try {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER);
fail("Expecting IllegalStateException"); fail("Expecting IllegalStateException");
@@ -594,14 +521,14 @@ public class TrackRecordingServiceTest
// Expected. // Expected.
} }
} }
@MediumTest @MediumTest
public void testInsertWaypointMarker_validWaypoint() throws Exception { public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(123, -1, true); createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER)); assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER));
Waypoint wpt = providerUtils.getWaypoint(1); Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.waypoint_icon_url), assertEquals(getContext().getString(R.string.waypoint_icon_url),
@@ -614,7 +541,7 @@ public class TrackRecordingServiceTest
assertNotNull(wpt.getLocation()); assertNotNull(wpt.getLocation());
assertNull(wpt.getStatistics()); assertNull(wpt.getStatistics());
} }
@MediumTest @MediumTest
public void testWithProperties_noAnnouncementFreq() throws Exception { public void testWithProperties_noAnnouncementFreq() throws Exception {
functionalTest(R.string.announcement_frequency_key, (Object) null); functionalTest(R.string.announcement_frequency_key, (Object) null);
@@ -624,7 +551,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_defaultAnnouncementFreq() throws Exception { public void testWithProperties_defaultAnnouncementFreq() throws Exception {
functionalTest(R.string.announcement_frequency_key, 1); functionalTest(R.string.announcement_frequency_key, 1);
} }
@MediumTest @MediumTest
public void testWithProperties_noMaxRecordingDist() throws Exception { public void testWithProperties_noMaxRecordingDist() throws Exception {
functionalTest(R.string.max_recording_distance_key, (Object) null); functionalTest(R.string.max_recording_distance_key, (Object) null);
@@ -654,7 +581,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_defaultSplitFreqByDist() throws Exception { public void testWithProperties_defaultSplitFreqByDist() throws Exception {
functionalTest(R.string.split_frequency_key, 5); functionalTest(R.string.split_frequency_key, 5);
} }
@MediumTest @MediumTest
public void testWithProperties_defaultSplitFreqByTime() throws Exception { public void testWithProperties_defaultSplitFreqByTime() throws Exception {
functionalTest(R.string.split_frequency_key, -2); functionalTest(R.string.split_frequency_key, -2);
@@ -674,7 +601,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_metricUnitsDisabled() throws Exception { public void testWithProperties_metricUnitsDisabled() throws Exception {
functionalTest(R.string.metric_units_key, false); functionalTest(R.string.metric_units_key, false);
} }
@MediumTest @MediumTest
public void testWithProperties_noMinRecordingInterval() throws Exception { public void testWithProperties_noMinRecordingInterval() throws Exception {
functionalTest(R.string.min_recording_interval_key, (Object) null); functionalTest(R.string.min_recording_interval_key, (Object) null);
@@ -690,23 +617,23 @@ public class TrackRecordingServiceTest
public void testWithProperties_noMinRequiredAccuracy() throws Exception { public void testWithProperties_noMinRequiredAccuracy() throws Exception {
functionalTest(R.string.min_required_accuracy_key, (Object) null); functionalTest(R.string.min_required_accuracy_key, (Object) null);
} }
@MediumTest @MediumTest
public void testWithProperties_defaultMinRequiredAccuracy() throws Exception { public void testWithProperties_defaultMinRequiredAccuracy() throws Exception {
functionalTest(R.string.min_required_accuracy_key, 500); functionalTest(R.string.min_required_accuracy_key, 500);
} }
@MediumTest @MediumTest
public void testWithProperties_noSensorType() throws Exception { public void testWithProperties_noSensorType() throws Exception {
functionalTest(R.string.sensor_type_key, (Object) null); functionalTest(R.string.sensor_type_key, (Object) null);
} }
@MediumTest @MediumTest
public void testWithProperties_zephyrSensorType() throws Exception { public void testWithProperties_zephyrSensorType() throws Exception {
functionalTest(R.string.sensor_type_key, 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) { private ITrackRecordingService bindAndGetService(Intent intent) {
ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( ITrackRecordingService service = ITrackRecordingService.Stub.asInterface(
bindService(intent)); bindService(intent));
@@ -724,7 +651,7 @@ public class TrackRecordingServiceTest
addTrack(dummyTrack, isRecording); addTrack(dummyTrack, isRecording);
return dummyTrack; return dummyTrack;
} }
private void updateAutoResumePrefs(int attempts, int timeoutMins) { private void updateAutoResumePrefs(int attempts, int timeoutMins) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putInt(context.getString( editor.putInt(context.getString(
@@ -733,20 +660,20 @@ public class TrackRecordingServiceTest
R.string.auto_resume_track_timeout_key), timeoutMins); R.string.auto_resume_track_timeout_key), timeoutMins);
editor.commit(); editor.commit();
} }
private Intent createStartIntent() { private Intent createStartIntent() {
Intent startIntent = new Intent(); Intent startIntent = new Intent();
startIntent.setClass(context, TrackRecordingService.class); startIntent.setClass(context, TrackRecordingService.class);
return startIntent; return startIntent;
} }
private void addTrack(Track track, boolean isRecording) { private void addTrack(Track track, boolean isRecording) {
assertTrue(track.getId() >= 0); assertTrue(track.getId() >= 0);
providerUtils.insertTrack(track); providerUtils.insertTrack(track);
assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId()); assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
setRecordingTrack(isRecording ? track.getId() : -1); setRecordingTrack(isRecording ? track.getId() : -1);
} }
private void setRecordingTrack(long id) { private void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putLong(context.getString(R.string.recording_track_key), id); editor.putLong(context.getString(R.string.recording_track_key), id);
@@ -760,11 +687,11 @@ public class TrackRecordingServiceTest
throws Exception { throws Exception {
final String key = context.getString(resourceId); final String key = context.getString(resourceId);
for (Object value : values) { 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 editor = sharedPreferences.edit();
editor.clear(); editor.clear();
if (value instanceof String) { if (value instanceof String) {
editor.putString(key, (String) value); editor.putString(key, (String) value);
} else if (value instanceof Long) { } else if (value instanceof Long) {
editor.putLong(key, (Long) value); editor.putLong(key, (Long) value);
} else if (value instanceof Integer) { } else if (value instanceof Integer) {
@@ -775,15 +702,15 @@ public class TrackRecordingServiceTest
// Do nothing, as clear above has already removed this property. // Do nothing, as clear above has already removed this property.
} }
editor.commit(); editor.commit();
fullRecordingSession(); fullRecordingSession();
} }
} }
private void fullRecordingSession() throws Exception { private void fullRecordingSession() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
// Start a track. // Start a track.
long id = service.startNewTrack(); long id = service.startNewTrack();
assertTrue(id >= 0); assertTrue(id >= 0);
@@ -806,14 +733,14 @@ public class TrackRecordingServiceTest
loc.setTime(startTime + i * 10000); loc.setTime(startTime + i * 10000);
loc.setBearing(3.0f); loc.setBearing(3.0f);
service.recordLocation(loc); service.recordLocation(loc);
if (i % 10 == 0) { if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) { } else if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER);
} }
} }
// Stop the track. Validate if it has correct data. // Stop the track. Validate if it has correct data.
service.endCurrentTrack(); service.endCurrentTrack();
assertFalse(service.isRecording()); assertFalse(service.isRecording());