Merging Sandor's changes.

This commit is contained in:
Rodrigo Damazio
2010-11-03 21:08:50 -02:00
26 changed files with 1708 additions and 279 deletions
+1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.maps.mytracks"
android:installLocation="auto"
android:versionCode="20"
android:versionName="1.0.19">
<application android:icon="@drawable/arrow_icon"
+9
View File
@@ -328,6 +328,7 @@ limitations under the License.
<string name="seconds">秒</string>
<!-- Distance -->
@@ -391,6 +392,14 @@ limitations under the License.
<!-- Send to Google -->
<string name="sendtogoogle_title">发送到...</string>
+2 -1
View File
@@ -38,6 +38,7 @@ limitations under the License.
<string name="share_url_only_key">shareUrlOnly</string>
<string name="signal_sampling_frequency_key">signalSamplingFrequency</string>
<string name="split_frequency_key">splitFrequency</string>
<string name="timestamp_track_name_key">timestampTrackName</string>
<!-- Preference keys which keep transient state (not user preferences) - DO NOT TRANSLATE -->
<string name="auto_resume_track_current_retry_key">autoResumeTrackCurrentRetry</string>
@@ -47,4 +48,4 @@ limitations under the License.
<!-- Keys for non-persisted preferences - DO NOT TRANSLATE -->
<string name="backup_to_sd_key">backupToSd</string>
<string name="restore_from_sd_key">restoreFromSd</string>
</resources>
</resources>
+4
View File
@@ -210,6 +210,10 @@ limitations under the License.
<string name="settings_auto_resume_track_timeout">Auto-resume track timeout</string>
<string name="settings_auto_resume_track_timeout_summary">Maximum time in minutes after which a previously recording track will not be resumed on phone reboot</string>
<string name="settings_timestamp_track_name">Timestamp as track name</string>
<string name="settings_timestamp_track_name_summary_on">The default track name will be the date and time at the start of the track</string>
<string name="settings_timestamp_track_name_summary_off">Track names will have numbers which increment for each new track.</string>
<string name="pace_label">Pace</string>
<string name="eclair_required">Only supported in Android 2.0 or above</string>
+10 -2
View File
@@ -91,7 +91,7 @@ limitations under the License.
android:entries="@array/task_frequency_options"
android:entryValues="@array/task_frequency_values"
android:defaultValue="-1" />
<com.google.android.apps.mytracks.IntegerListPreference
android:key="@string/auto_resume_track_timeout_key"
android:title="@string/settings_auto_resume_track_timeout"
@@ -99,6 +99,14 @@ limitations under the License.
android:entries="@array/auto_resume_track_timeout_options"
android:entryValues="@array/auto_resume_track_timeout_values"
android:defaultValue="10" />
<CheckBoxPreference
android:key="@string/timestamp_track_name_key"
android:title="@string/settings_timestamp_track_name"
android:summaryOn="@string/settings_timestamp_track_name_summary_on"
android:summaryOff="@string/settings_timestamp_track_name_summary_off"
android:defaultValue="true" />
</PreferenceScreen>
<!-- Sharing settings -->
@@ -126,7 +134,7 @@ limitations under the License.
android:title="@string/settings_backup_to_sd"
android:summary="@string/settings_backup_to_sd_summary"
android:persistent="false" />
<Preference
<Preference
android:key="@string/restore_from_sd_key"
android:title="@string/settings_restore_from_sd"
android:summary="@string/settings_restore_from_sd_summary"
@@ -298,13 +298,13 @@ public class ChartActivity extends Activity implements
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG, "ChartActivity: ContentObserver.onChange");
// Check for any new locations and append them to the currently
// recording track:
if (!MyTracks.getInstance().isRecording()) {
// recording track.
if (recordingTrackId < 0) {
// No track is being recorded. We should not be here.
return;
}
if (selectedTrackId != recordingTrackId) {
// no track, or one other than the recording track is selected, don't
// No track, or one other than the recording track is selected, don't
// bother.
return;
}
@@ -34,6 +34,7 @@ import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.TempFileCleaner;
import com.google.android.apps.mytracks.io.TrackWriter;
import com.google.android.apps.mytracks.io.TrackWriterFactory;
import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
@@ -67,9 +68,9 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.View.OnTouchListener;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager.BadTokenException;
import android.widget.RelativeLayout;
import android.widget.TabHost;
@@ -99,9 +100,10 @@ public class MyTracks extends TabActivity implements OnTouchListener,
/**
* Singleton instance
*/
private static MyTracks instance = null;
private ChartActivity chartActivity = null;
private static MyTracks instance;
private ChartActivity chartActivity;
public ChartActivity getChartActivity() {
return chartActivity;
}
@@ -166,6 +168,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
* Utilities to deal with the database.
*/
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
/**
* The connection to the track recording service.
@@ -221,20 +225,35 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}
/**
* Checks whether we have a track recording session in progress.
* In some cases, when the service has crashed or has been restarted
* by the system, we fall back to the shared preferences.
*
* @return true if the activity is bound to the track recording service and
* the service is recording a track.
* the service is recording a track or in case the service is down,
* based on settings from the shared preferences.
*/
public boolean isRecording() {
if (trackRecordingService == null) {
return false;
// Fall back to alternative check method.
return isRecordingBasedOnSharedPreferences();
}
try {
return trackRecordingService.isRecording();
} catch (RemoteException e) {
Log.e(MyTracksConstants.TAG, "MyTracks: Remote exception.", e);
return false;
// Fall back to alternative check method.
return isRecordingBasedOnSharedPreferences();
}
}
private boolean isRecordingBasedOnSharedPreferences() {
// TrackRecordingServices guarantees that recordingTrackId is set to
// -1 if the track has been stopped.
// TODO: Refresh recordingTrackId.
return recordingTrackId >= 0;
}
/*
* Application lifetime events:
@@ -248,6 +267,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
instance = this;
providerUtils = MyTracksProviderUtils.Factory.get(this);
menuManager = new MenuManager(this);
sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
dialogManager = new DialogManager(this);
// The volume we want to control is the Text-To-Speech volume
@@ -285,14 +305,18 @@ public class MyTracks extends TabActivity implements OnTouchListener,
tabHost.addView(layout);
layout.setOnTouchListener(this);
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
if (sharedPreferences != null) {
selectedTrackId =
prefs.getLong(getString(R.string.selected_track_key), -1);
recordingTrackId =
prefs.getLong(getString(R.string.recording_track_key), -1);
prefs.registerOnSharedPreferenceChangeListener(this);
sharedPreferences.getLong(getString(R.string.selected_track_key), -1);
recordingTrackId = sharedPreferences.getLong(
getString(R.string.recording_track_key), -1);
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
Log.d(MyTracksConstants.TAG, "recordingTrackId: " + recordingTrackId
+ ", selectedTrackId: " + selectedTrackId);
if (recordingTrackId > 0) {
Intent startIntent = new Intent(this, TrackRecordingService.class);
startService(startIntent);
}
}
// This will show the eula until the user accepts or quits the app.
@@ -850,20 +874,15 @@ public class MyTracks extends TabActivity implements OnTouchListener,
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
getResources().getText(R.string.share_map_subject).toString());
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean shareUrlOnly = true;
if (prefs != null) {
shareUrlOnly =
prefs.getBoolean(getString(R.string.share_url_only_key), false);
if (sharedPreferences != null) {
shareUrlOnly = sharedPreferences.getBoolean(
getString(R.string.share_url_only_key), false);
}
String url = MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
String msg = (shareUrlOnly
? url
: String.format(
getResources().getText(R.string.share_map_body_format).toString(),
url));
String msg = shareUrlOnly ? url : String.format(
getResources().getText(R.string.share_map_body_format).toString(), url);
shareIntent.putExtra(Intent.EXTRA_TEXT, msg);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_map).toString()));
@@ -1077,9 +1096,10 @@ public class MyTracks extends TabActivity implements OnTouchListener,
private void startRecordingNewTrack() {
try {
recordingTrackId = trackRecordingService.startNewTrack();
// Select the recording track.
setSelectedTrack(recordingTrackId);
Toast.makeText(this, getString(R.string.status_now_recording),
Toast.LENGTH_SHORT).show();
setSelectedAndRecordingTrack(recordingTrackId, recordingTrackId);
} catch (RemoteException e) {
Toast.makeText(this,
getString(R.string.error_unable_to_start_recording),
@@ -1114,7 +1134,6 @@ public class MyTracks extends TabActivity implements OnTouchListener,
} catch (RemoteException e) {
Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e);
}
setRecordingTrack(-1);
Intent intent = new Intent(MyTracks.this, MyTracksDetails.class);
intent.putExtra("trackid", recordingTrackId);
intent.putExtra("hasCancelButton", false);
@@ -1171,9 +1190,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
public void setSelectedTrack(final long trackId) {
runOnUiThread(new Runnable() {
public void run() {
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
SharedPreferences.Editor editor = prefs.edit();
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putLong(getString(R.string.selected_track_key), trackId);
editor.commit();
}
@@ -1184,46 +1201,6 @@ public class MyTracks extends TabActivity implements OnTouchListener,
return selectedTrackId;
}
/**
* Writes the recording track id to the shared preferences.
* Executed on the UI thread.
*
* @param trackId the id of the track
*/
private void setRecordingTrack(final long trackId) {
runOnUiThread(new Runnable() {
public void run() {
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(getString(R.string.recording_track_key), trackId);
editor.commit();
}
});
}
/**
* Writes the selected and the recording track id to the shared preferences.
* Executed on UI thread.
*/
private void setSelectedAndRecordingTrack(final long theSelectedTrackId,
final long theRecordingTrackId) {
runOnUiThread(new Runnable() {
public void run() {
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(getString(R.string.selected_track_key),
theSelectedTrackId);
editor.putLong(getString(R.string.recording_track_key),
theRecordingTrackId);
editor.commit();
}
}
});
}
/**
* Binds to track recording service if it is running.
*/
@@ -118,7 +118,7 @@ public class MyTracksMap extends MapActivity
private double variation;
/**
* From the shared preferences:
* From the shared preferences.
*/
private int minRequiredAccuracy =
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
@@ -135,8 +135,8 @@ public class MyTracksMap extends MapActivity
private Location currentLocation;
/**
* A thread with a looper. Post to updateTrackHandler to execute Runnables on
* this thread.
* A thread with a looper. Post to updateTrackHandler to execute
* {@link Runnable}s on this thread.
*/
private HandlerThread updateTrackThread;
@@ -146,6 +146,8 @@ public class MyTracksMap extends MapActivity
private Handler updateTrackHandler;
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
/**
* A runnable that updates the track from the provider (looking for points
@@ -331,12 +333,6 @@ public class MyTracksMap extends MapActivity
@Override
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange");
// Check for any new locations and append them to the currently
// recording track:
if (!MyTracks.getInstance().isRecording()) {
// No track is being recorded. We should not be here.
return;
}
if (!isRecordingSelected()) {
// No track, or one other than the recording track is selected,
// don't bother.
@@ -361,30 +357,25 @@ public class MyTracksMap extends MapActivity
}
};
// Read shared preferences and register change listener:
SharedPreferences preferences =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (preferences != null) {
minRequiredAccuracy = preferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
recordingTrackId =
preferences.getLong(getString(R.string.recording_track_key), -1);
long selectedTrackId =
preferences.getLong(getString(R.string.selected_track_key), -1);
if (selectedTrackId >= 0) {
setSelectedTrack(selectedTrackId);
}
// Read shared preferences and register change listener.
sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences != null) {
reloadSharedPreferences(sharedPreferences, null);
updateOptionsButton();
preferences.registerOnSharedPreferenceChangeListener(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
}
}
@Override
protected void onDestroy() {
Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy");
if (updateTrackThread != null) {
updateTrackThread.quit();
ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread(
updateTrackThread);
}
if (sharedPreferences != null) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
super.onDestroy();
@@ -451,6 +442,9 @@ public class MyTracksMap extends MapActivity
Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume");
super.onResume();
// Reload all preferences as they might have changed since last run.
reloadSharedPreferences(sharedPreferences, null);
// Make sure any updates that might have happened are propagated to the
// Map overlay:
observer.onChange(false);
@@ -707,11 +701,14 @@ public class MyTracksMap extends MapActivity
* @param trackId a given track id
*/
public void setSelectedTrack(final long trackId) {
Log.d(MyTracksConstants.TAG, "MyTracksMap.setSelectedTrack: "
+ "selectedtTrackId = " + selectedTrackId + ", trackId = " + trackId);
if (selectedTrackId == trackId) {
// Selected track did not change, nothing to do.
mapOverlay.setTrackDrawingEnabled(isATrackSelected());
mapView.invalidate();
updateOptionsButton();
mapView.invalidate();
return;
}
@@ -786,7 +783,7 @@ public class MyTracksMap extends MapActivity
menu.setHeaderTitle(R.string.tracklist_this_track);
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
R.string.tracklist_edit_track);
if (!MyTracks.getInstance().isRecording() || !isRecordingSelected()) {
if (!isRecordingSelected()) {
menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0,
@@ -906,30 +903,13 @@ public class MyTracksMap extends MapActivity
@Override
public void onSharedPreferenceChanged(
final SharedPreferences sharedPreferences, final String key) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap.onSharedPreferenceChanged: " + key);
if (key != null) {
uiHandler.post(new Runnable() {
@Override
public void run() {
if (key.equals(getString(R.string.min_required_accuracy_key))) {
minRequiredAccuracy = sharedPreferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
} else if (key.equals(getString(R.string.selected_track_key))) {
long selectedTrackId =
sharedPreferences.getLong(
getString(R.string.selected_track_key),
-1);
setSelectedTrack(selectedTrackId);
} else if (key.equals(getString(R.string.recording_track_key))) {
recordingTrackId =
sharedPreferences.getLong(
getString(R.string.recording_track_key),
-1);
if (isATrackSelected()) {
mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.postInvalidate();
}
}
reloadSharedPreferences(sharedPreferences, key);
}
});
}
@@ -1009,6 +989,31 @@ public class MyTracksMap extends MapActivity
}
};
private void reloadSharedPreferences(SharedPreferences sharedPreferences,
String key) {
if (key == null ||
key.equals(getString(R.string.min_required_accuracy_key))) {
minRequiredAccuracy = sharedPreferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
}
if (key == null || key.equals(getString(R.string.recording_track_key))) {
recordingTrackId = sharedPreferences.getLong(
getString(R.string.recording_track_key), -1);
}
if (key == null || key.equals(getString(R.string.selected_track_key))) {
setSelectedTrack(sharedPreferences.getLong(
getString(R.string.selected_track_key), -1));
}
// Show end marker if the track has been selected and is not recording.
// Note: This check must be *after* a call to setSelectedTrack(...) above.
if (isATrackSelected()) {
mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.postInvalidate();
}
}
private void readAllNewTrackPoints() {
int numPoints = mapOverlay.getNumLocations();
if (numPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) {
@@ -52,7 +52,7 @@ import android.widget.Toast;
public class StatsActivity extends Activity
implements OnSharedPreferenceChangeListener {
private final StatsUtilities utils;
private StatsUtilities utils;
private UIUpdateThread thread;
private ContentObserver observer;
@@ -94,7 +94,7 @@ public class StatsActivity extends Activity
*/
private boolean showCurrentSegment = false;
private final MyTracksProviderUtils providerUtils;
private MyTracksProviderUtils providerUtils;
/**
* A runnable for posting to the UI thread. Will update the total time field.
@@ -133,16 +133,14 @@ public class StatsActivity extends Activity
}
}
public StatsActivity() {
utils = new StatsUtilities(this);
providerUtils = new MyTracksProviderUtilsImpl(this);
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
utils = new StatsUtilities(this);
providerUtils = new MyTracksProviderUtilsImpl(getContentResolver());
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
@@ -345,7 +345,7 @@ public interface MyTracksProviderUtils {
* Creates an instance of {@link MyTracksProviderUtils}.
*/
protected MyTracksProviderUtils newForContext(Context context) {
return new MyTracksProviderUtilsImpl(context);
return new MyTracksProviderUtilsImpl(context.getContentResolver());
}
}
}
@@ -17,8 +17,8 @@ package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
@@ -35,10 +35,10 @@ import java.util.List;
*/
public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private final Context context;
private final ContentResolver contentResolver;
public MyTracksProviderUtilsImpl(Context context) {
this.context = context;
public MyTracksProviderUtilsImpl(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
/**
@@ -448,25 +448,22 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public void deleteAllTracks() {
context.getContentResolver().delete(TracksColumns.CONTENT_URI, null, null);
context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI,
null, null);
context.getContentResolver().delete(
WaypointsColumns.CONTENT_URI, null, null);
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
}
@Override
public void deleteTrack(long trackId) {
Track track = getTrack(trackId);
if (track != null) {
context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI,
contentResolver.delete(TrackPointsColumns.CONTENT_URI,
"_id>=" + track.getStartId() + " AND _id<=" + track.getStopId(),
null);
}
context.getContentResolver().delete(WaypointsColumns.CONTENT_URI,
contentResolver.delete(WaypointsColumns.CONTENT_URI,
WaypointsColumns.TRACKID + "=" + trackId, null);
context.getContentResolver().delete(
TracksColumns.CONTENT_URI, "_id=" + trackId, null);
contentResolver.delete(TracksColumns.CONTENT_URI, "_id=" + trackId, null);
}
@Override
@@ -491,7 +488,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
"No statistics marker after the deleted one was found.");
}
}
context.getContentResolver().delete(
contentResolver.delete(
WaypointsColumns.CONTENT_URI, "_id=" + waypointId, null);
}
@@ -503,7 +500,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
final String sortOrder = WaypointsColumns._ID + " LIMIT 1";
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
selection,
@@ -525,7 +522,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public boolean updateWaypoint(Waypoint waypoint) {
try {
final int rows = context.getContentResolver().update(
final int rows = contentResolver.update(
WaypointsColumns.CONTENT_URI,
createContentValues(waypoint),
"_id=" + waypoint.getId(),
@@ -546,7 +543,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private Location findLocationBy(String select) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, select, null, null);
if (cursor != null && cursor.moveToNext()) {
return createLocation(cursor);
@@ -570,7 +567,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private Track findTrackBy(String select) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, select, null, null);
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
@@ -592,7 +589,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Waypoint getFirstWaypoint(long trackId) {
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
"trackid=" + trackId,
@@ -614,7 +611,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Waypoint getWaypoint(long waypointId) {
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
"_id=" + waypointId,
@@ -637,7 +634,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getLastLocationId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI,
projection,
"_id=(select max(_id) from trackpoints WHERE trackid=" + trackId + ")",
@@ -661,7 +658,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getFirstWaypointId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
projection,
"trackid=" + trackId,
@@ -685,7 +682,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getLastWaypointId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
projection,
WaypointsColumns.TRACKID + "=" + trackId,
@@ -710,7 +707,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
public Track getLastTrack() {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, "_id=(select max(_id) from tracks)",
null, null);
if (cursor != null && cursor.moveToNext()) {
@@ -729,7 +726,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getLastTrackId() {
String[] proj = { TracksColumns._ID };
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, proj, "_id=(select max(_id) from tracks)",
null, null);
if (cursor != null) {
@@ -769,7 +766,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
sortOrder += " LIMIT " + maxLocations;
}
return context.getContentResolver().query(
return contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder);
}
@@ -791,7 +788,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
sortOrder += " LIMIT " + maxWaypoints;
}
return context.getContentResolver().query(
return contentResolver.query(
WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder);
}
@@ -847,7 +844,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Cursor getTracksCursor(String selection) {
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, selection, null, "_id");
return cursor;
}
@@ -855,14 +852,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Uri insertTrack(Track track) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrack");
return context.getContentResolver().insert(TracksColumns.CONTENT_URI,
return contentResolver.insert(TracksColumns.CONTENT_URI,
createContentValues(track));
}
@Override
public Uri insertTrackPoint(Location location, long trackId) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrackPoint");
return context.getContentResolver().insert(TrackPointsColumns.CONTENT_URI,
return contentResolver.insert(TrackPointsColumns.CONTENT_URI,
createContentValues(location, trackId));
}
@@ -875,14 +872,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
values[i] = createContentValues(locations[i], trackId);
}
return context.getContentResolver().bulkInsert(TrackPointsColumns.CONTENT_URI, values);
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values);
}
@Override
public Uri insertWaypoint(Waypoint waypoint) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint");
waypoint.setId(-1);
return context.getContentResolver().insert(WaypointsColumns.CONTENT_URI,
return contentResolver.insert(WaypointsColumns.CONTENT_URI,
createContentValues(waypoint));
}
@@ -891,7 +888,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
Cursor cursor = null;
try {
final String[] projection = { TracksColumns._ID };
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TracksColumns.CONTENT_URI,
projection,
TracksColumns._ID + "=" + id/*selection*/,
@@ -911,7 +908,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public void updateTrack(Track track) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.updateTrack");
context.getContentResolver().update(TracksColumns.CONTENT_URI,
contentResolver.update(TracksColumns.CONTENT_URI,
createContentValues(track), "_id=" + track.getId(), null);
}
}
@@ -468,8 +468,7 @@ public class GpxImporter extends DefaultHandler {
// initialize start time with time of first track point
if (statsBuilder == null) {
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(time);
statsBuilder = new TripStatisticsBuilder(time);
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.format.Time;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Creates a default track name based on the current default track name policy.
*
* @author Matthew Simmons
*/
class DefaultTrackNameFactory {
private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm";
private final Context context;
DefaultTrackNameFactory(Context context) {
this.context = context;
}
/**
* Creates a new track name.
*
* @param trackId The ID for the current track.
* @param startTime The start time, in milliseconds since the epoch, of the
* current track.
* @return The new track name.
*/
String newTrackName(long trackId, long startTime) {
if (useTimestampTrackName()) {
SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT);
return formatter.format(new Date(startTime));
} else {
return String.format(context.getString(R.string.new_track), trackId);
}
}
/** Determines whether the preferences allow a timestamp-based track name */
protected boolean useTimestampTrackName() {
SharedPreferences prefs =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
return prefs.getBoolean(
context.getString(R.string.timestamp_track_name_key), true);
}
}
@@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.maps.mytracks.R;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
/**
@@ -30,6 +31,7 @@ import android.util.Log;
public class PreferenceManager {
private TrackRecordingService service;
private final String announcementFrequencyKey;
private final String autoResumeTrackCurrentRetryKey;
private final String autoResumeTrackTimeoutKey;
private final String maxRecordingDistanceKey;
private final String metricUnitsKey;
@@ -40,11 +42,22 @@ public class PreferenceManager {
private final String signalSamplingFrequencyKey;
private final String splitFrequencyKey;
private final SharedPreferences sharedPreferences;
public PreferenceManager(TrackRecordingService service) {
this.service = service;
this.sharedPreferences =
service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences == null) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService: Couldn't get shared preferences.");
throw new IllegalStateException("Couldn't get shared preferences");
}
announcementFrequencyKey =
service.getString(R.string.announcement_frequency_key);
autoResumeTrackCurrentRetryKey =
service.getString(R.string.auto_resume_track_current_retry_key);
autoResumeTrackTimeoutKey =
service.getString(R.string.auto_resume_track_timeout_key);
maxRecordingDistanceKey =
@@ -57,12 +70,12 @@ public class PreferenceManager {
service.getString(R.string.min_recording_interval_key);
minRequiredAccuracyKey =
service.getString(R.string.min_required_accuracy_key);
splitFrequencyKey =
service.getString(R.string.split_frequency_key);
signalSamplingFrequencyKey =
service.getString(R.string.signal_sampling_frequency_key);
recordingTrackKey =
service.getString(R.string.recording_track_key);
signalSamplingFrequencyKey =
service.getString(R.string.signal_sampling_frequency_key);
splitFrequencyKey =
service.getString(R.string.split_frequency_key);
}
/**
@@ -72,14 +85,6 @@ public class PreferenceManager {
* @param key the key that changed (may be null to update all preferences)
*/
public void onSharedPreferenceChanged(String key) {
SharedPreferences sharedPreferences =
service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences == null) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService: Couldn't get shared preferences.");
return;
}
if (key == null || key.equals(minRecordingDistanceKey)) {
service.setMinRecordingDistance(
sharedPreferences.getInt(
@@ -156,4 +161,16 @@ public class PreferenceManager {
sharedPreferences.getBoolean(metricUnitsKey, true));
}
}
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
editor.commit();
}
public void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(recordingTrackKey, id);
editor.commit();
}
}
@@ -43,7 +43,7 @@ public class StatusAnnouncerTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
private static final float TTS_SPEECH_RATE = 0.9f;
static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
@@ -58,7 +58,7 @@ public class StatusAnnouncerTask implements PeriodicTask {
/**
* The interface to the text to speech engine.
*/
private final TextToSpeech tts;
private TextToSpeech tts;
/**
* The response recieved from the TTS engine ater initialization.
@@ -85,21 +85,13 @@ public class StatusAnnouncerTask implements PeriodicTask {
}
};
/**
* Constructs the announcer and start the TTS engine.
*/
public StatusAnnouncerTask(Context context) {
this.context = context;
this.stringUtils = new StringUtils(context);
this(context, new StringUtils(context));
}
// We can't have this class also be the listener, otherwise it's unsafe to
// reference it in Cupcake (even if we don't instantiate it).
tts = new TextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
onTtsInit(status);
}
});
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
this.context = context;
this.stringUtils = stringUtils;
}
/**
@@ -129,11 +121,13 @@ public class StatusAnnouncerTask implements PeriodicTask {
}
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (!ready) {
if (!ready || tts == null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready.");
return;
}
@@ -159,7 +153,8 @@ public class StatusAnnouncerTask implements PeriodicTask {
*
* @return The string that will be read to the user
*/
private String getAnnouncement(final TripStatistics stats) {
// @VisibleForTesting
protected String getAnnouncement(TripStatistics stats) {
SharedPreferences preferences =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
@@ -223,23 +218,53 @@ public class StatusAnnouncerTask implements PeriodicTask {
@Override
public void start() {
// Register ourselves as a listener so we won't speak during a call.
if (tts == null) {
// We can't have this class also be the listener, otherwise it's unsafe to
// reference it in Cupcake (even if we don't instantiate it).
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
onTtsInit(status);
}
});
}
speechAllowed = true;
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telephony.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
// Register ourselves as a listener so we won't speak during a call.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void shutdown() {
// Stop listening to phone state.
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telephony.listen(phoneListener, PhoneStateListener.LISTEN_NONE);
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
tts.shutdown();
tts = null;
}
/**
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
* several issues during testing.
*/
// @VisibleForTesting
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return new TextToSpeech(ctx, onInitListener);
}
/**
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
*/
// @VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telephony.listen(listener, events);
}
/**
* Returns the volume stream to use for controlling announcement volume.
*/
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
@@ -28,6 +28,7 @@ import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiPlatformAdapter;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
@@ -51,8 +52,10 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.text.format.DateFormat;
import android.util.Log;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
@@ -67,7 +70,7 @@ public class TrackRecordingService extends Service implements LocationListener {
private static final String STATISTICS_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png";
private static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private NotificationManager notificationManager;
private LocationManager locationManager;
@@ -94,8 +97,8 @@ public class TrackRecordingService extends Service implements LocationListener {
*/
private MyTracksProviderUtils providerUtils;
private TripStatisticsBuilder statsBuilder = new TripStatisticsBuilder();
private TripStatisticsBuilder waypointStatsBuilder = new TripStatisticsBuilder();
private TripStatisticsBuilder statsBuilder;
private TripStatisticsBuilder waypointStatsBuilder;
/**
* Current length of the recorded track. This length is calculated from the
@@ -329,6 +332,8 @@ public class TrackRecordingService extends Service implements LocationListener {
* Shows the notification message and icon in the notification bar.
*/
public void showNotification() {
final ApiPlatformAdapter apiPlatformAdapter =
ApiFeatures.getInstance().getApiPlatformAdapter();
if (isRecording) {
Notification notification = new Notification(
R.drawable.arrow_320, null /* tickerText */,
@@ -339,9 +344,10 @@ public class TrackRecordingService extends Service implements LocationListener {
notification.setLatestEventInfo(this, getString(R.string.app_name),
getString(R.string.recording_your_track), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR;
notificationManager.notify(1, notification);
apiPlatformAdapter.startForeground(this, notificationManager, 1,
notification);
} else {
notificationManager.cancelAll();
apiPlatformAdapter.stopForeground(this, notificationManager, 1);
}
}
@@ -390,9 +396,11 @@ public class TrackRecordingService extends Service implements LocationListener {
}
private void restoreStats(Track track) {
Log.d(MyTracksConstants.TAG,
"Restoring stats of track with ID: " + track.getId());
TripStatistics stats = track.getStatistics();
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(stats.getStartTime());
statsBuilder = new TripStatisticsBuilder(stats.getStartTime());
setUpAnnouncer();
signalManager.restore();
@@ -403,11 +411,11 @@ public class TrackRecordingService extends Service implements LocationListener {
Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
if (waypoint != null) {
currentWaypointId = waypoint.getId();
waypointStatsBuilder = new TripStatisticsBuilder(waypoint.getStatistics());
waypointStatsBuilder = new TripStatisticsBuilder(
waypoint.getStatistics());
} else {
// This should never happen, but we got to do something so life goes on:
waypointStatsBuilder = new TripStatisticsBuilder();
waypointStatsBuilder.resumeAt(stats.getStartTime());
waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime());
currentWaypointId = -1;
}
@@ -431,7 +439,7 @@ public class TrackRecordingService extends Service implements LocationListener {
}
statsBuilder.getStatistics().setMovingTime(stats.getMovingTime());
statsBuilder.pauseAt(stats.getStopTime());
statsBuilder.resume();
statsBuilder.resumeAt(System.currentTimeMillis());
} else {
Log.e(MyTracksConstants.TAG, "Could not get track points cursor.");
}
@@ -656,6 +664,11 @@ public class TrackRecordingService extends Service implements LocationListener {
if (recordingTrack != null) {
restoreStats(recordingTrack);
isRecording = true;
} else {
// Make sure we have consistent state in shared preferences.
Log.w(MyTracksConstants.TAG, "TrackRecordingService.onCreate: Resetting "
+ "an orphaned recording track: " + recordingTrackId);
prefManager.setRecordingTrack(recordingTrackId = -1);
}
showNotification();
}
@@ -739,8 +752,10 @@ public class TrackRecordingService extends Service implements LocationListener {
// Make sure that the current track exists and is fresh enough.
if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) {
Log.i(MyTracksConstants.TAG,
"TrackRecordingService: Not resuming because the previous track "
+ "doesn't exist or is too old");
"TrackRecordingService: Not resuming, because the previous track ("
+ recordingTrack + ") doesn't exist or is too old");
isRecording = false;
prefManager.setRecordingTrack(recordingTrackId = -1);
stopSelfResult(startId);
return;
}
@@ -753,16 +768,12 @@ public class TrackRecordingService extends Service implements LocationListener {
SharedPreferences sharedPreferences, int retryAttempts) {
Log.d(MyTracksConstants.TAG,
"Updating auto-resume retry attempts to: " + retryAttempts);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt(
getString(R.string.auto_resume_track_current_retry_key), retryAttempts);
editor.commit();
prefManager.setAutoResumeTrackCurrentRetry(retryAttempts);
}
private boolean shouldResumeTrack(Track track) {
Log.d(MyTracksConstants.TAG,
"maybeResumeTrack: autoResumeTrackTimeout = " + autoResumeTrackTimeout);
Log.d(MyTracksConstants.TAG, "shouldResumeTrack: autoResumeTrackTimeout = "
+ autoResumeTrackTimeout);
// Check if we haven't exceeded the maximum number of retry attempts.
SharedPreferences sharedPreferences =
@@ -770,11 +781,11 @@ public class TrackRecordingService extends Service implements LocationListener {
int retries = sharedPreferences.getInt(
getString(R.string.auto_resume_track_current_retry_key), 0);
Log.d(MyTracksConstants.TAG,
"TrackRecordingService: Attempting to auto-resume the track ("
"shouldResumeTrack: Attempting to auto-resume the track ("
+ (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")");
if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
Log.i(MyTracksConstants.TAG,
"TrackRecordingService: Not resuming because exceeded the maximum "
"shouldResumeTrack: Not resuming because exceeded the maximum "
+ "number of auto-resume retries");
return false;
}
@@ -785,9 +796,13 @@ public class TrackRecordingService extends Service implements LocationListener {
// Check for special cases.
if (autoResumeTrackTimeout == 0) {
// Never resume.
Log.d(MyTracksConstants.TAG,
"shouldResumeTrack: Auto-resume disabled (never resume)");
return false;
} else if (autoResumeTrackTimeout == -1) {
// Always resume.
Log.d(MyTracksConstants.TAG,
"shouldResumeTrack: Auto-resume forced (always resume)");
return true;
}
@@ -795,7 +810,8 @@ public class TrackRecordingService extends Service implements LocationListener {
long lastModified =
track.getStatistics() != null ? track.getStatistics().getStopTime() : 0;
Log.d(MyTracksConstants.TAG,
"maybeResumeTrack: lastModified = " + lastModified);
"shouldResumeTrack: lastModified = " + lastModified
+ ", autoResumeTrackTimeout: " + autoResumeTrackTimeout);
return lastModified > 0 && System.currentTimeMillis() - lastModified <=
autoResumeTrackTimeout * 60 * 1000;
}
@@ -805,6 +821,11 @@ public class TrackRecordingService extends Service implements LocationListener {
}
public long insertWaypointMarker(Waypoint waypoint) {
if (!isRecording()) {
throw new IllegalStateException(
"Unable to insert waypoint marker while not recording!");
}
if (waypoint.getLocation() != null) {
waypoint.setLength(length);
waypoint.setDuration(waypoint.getLocation().getTime()
@@ -823,6 +844,11 @@ public class TrackRecordingService extends Service implements LocationListener {
* @return the unique id of the inserted marker
*/
public long insertStatisticsMarker(Location location) {
if (!isRecording()) {
throw new IllegalStateException(
"Unable to insert statistics marker while not recording!");
}
StringUtils utils = new StringUtils(TrackRecordingService.this);
// Create a new waypoint to save
@@ -850,8 +876,7 @@ public class TrackRecordingService extends Service implements LocationListener {
Uri uri = providerUtils.insertWaypoint(waypoint);
// Create a new stats keeper for the next marker
waypointStatsBuilder = new TripStatisticsBuilder();
waypointStatsBuilder.resumeAt(time);
waypointStatsBuilder = new TripStatisticsBuilder(time);
updateCurrentWaypoint();
return Long.parseLong(uri.getLastPathSegment());
}
@@ -908,6 +933,10 @@ public class TrackRecordingService extends Service implements LocationListener {
@Override
public void endCurrentTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack");
if (recordingTrackId == -1 || !isRecording) {
throw new IllegalStateException("No recording track in progress!");
}
isRecording = false;
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
if (recordingTrack != null) {
@@ -927,12 +956,15 @@ public class TrackRecordingService extends Service implements LocationListener {
"_id=" + recordingTrack.getId(), null);
}
showNotification();
recordingTrackId = -1;
prefManager.setRecordingTrack(recordingTrackId = -1);
}
@Override
public void deleteAllTracks() {
endCurrentTrack();
if (isRecording()) {
throw new IllegalStateException(
"Cannot delete all tracks while recording!");
}
providerUtils.deleteAllTracks();
}
@@ -951,36 +983,45 @@ public class TrackRecordingService extends Service implements LocationListener {
public long startNewTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack");
if (recordingTrackId != -1 || isRecording) {
throw new IllegalStateException("A track is already in progress!");
}
long startTime = System.currentTimeMillis();
Track track = new Track();
TripStatistics trackStats = track.getStatistics();
track.setName("new");
long startTime = System.currentTimeMillis();
trackStats.setStartTime(startTime);
track.setStartId(-1);
Uri trackUri = providerUtils.insertTrack(track);
recordingTrackId = Long.parseLong(trackUri.getLastPathSegment());
track.setId(recordingTrackId);
track.setName(String.format(getString(R.string.new_track), recordingTrackId));
providerUtils.updateTrack(track);
currentWaypointId = insertStatisticsMarker(null);
track.setName(new DefaultTrackNameFactory(this).newTrackName(
recordingTrackId, startTime));
isRecording = true;
isMoving = true;
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(startTime);
providerUtils.updateTrack(track);
statsBuilder = new TripStatisticsBuilder(startTime);
waypointStatsBuilder = new TripStatisticsBuilder(startTime);
currentWaypointId = insertStatisticsMarker(null);
setUpAnnouncer();
length = 0;
showNotification();
registerLocationListener();
splitManager.restore();
signalManager.restore();
// Reset the number of auto-resume retries.
SharedPreferences sharedPreferences =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
setAutoResumeTrackRetries(sharedPreferences, 0);
setAutoResumeTrackRetries(
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0);
// Persist the current recording track.
prefManager.setRecordingTrack(recordingTrackId);
return recordingTrackId;
}
TripStatistics getTripStatistics() {
public TripStatistics getTripStatistics() {
return statsBuilder.getStatistics();
}
@@ -36,13 +36,13 @@ public class TripStatisticsBuilder {
/**
* The last location that the gps reported.
*/
private Location lastLocation = null;
private Location lastLocation;
/**
* The last location that contributed to the stats. It is also the last
* location the user was found to be moving.
*/
private Location lastMovingLocation = null;
private Location lastMovingLocation;
/**
* The current speed in meters/second as reported by the gps.
@@ -90,10 +90,13 @@ public class TripStatisticsBuilder {
private long totalLocations = 0;
/**
* Creates a new trip starting at the current system time.
* Creates a new trip starting at the given time.
*
* @param startTime the start time.
*/
public TripStatisticsBuilder() {
public TripStatisticsBuilder(long startTime) {
data = new TripStatistics();
resumeAt(startTime);
}
/**
@@ -103,6 +106,9 @@ public class TripStatisticsBuilder {
*/
public TripStatisticsBuilder(TripStatistics statsData) {
data = new TripStatistics(statsData);
if (data.getStartTime() > 0) {
resumeAt(data.getStartTime());
}
}
/**
@@ -140,8 +146,8 @@ public class TripStatisticsBuilder {
// Don't do anything if we didn't move since last fix:
double distance = lastLocation.distanceTo(currentLocation);
if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE
&& currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) {
if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE &&
currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) {
lastLocation = currentLocation;
return false;
}
@@ -291,13 +297,6 @@ public class TripStatisticsBuilder {
data.updateGradeExtremities(gradeBuffer.getAverage());
}
/**
* Pauses the track at the current time.
*/
public void pause() {
pauseAt(System.currentTimeMillis());
}
/**
* Pauses the track at the given time.
*
@@ -312,13 +311,6 @@ public class TripStatisticsBuilder {
paused = true;
}
/**
* Resumes the current track at the current time.
*/
public void resume() {
resumeAt(System.currentTimeMillis());
}
/**
* Resumes the current track at the given time.
*
@@ -364,8 +356,7 @@ public class TripStatisticsBuilder {
}
public TripStatistics getStatistics() {
// Take a snapshot - we do't want anyone messing with our internals
// Take a snapshot - we don't want anyone messing with our internals
return new TripStatistics(data);
}
}
@@ -15,7 +15,10 @@
*/
package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.os.Build;
import android.util.Log;
/**
* Utility class for determining if newer-API features are available on the
@@ -28,9 +31,15 @@ public class ApiFeatures {
/**
* The API level of the Android version we're being run under.
*/
public static final int ANDROID_API_LEVEL = Integer.parseInt(Build.VERSION.SDK);
public static final int ANDROID_API_LEVEL = Integer.parseInt(
Build.VERSION.SDK);
private static ApiFeatures instance;
/**
* The API platform adapter supported by this system.
*/
private ApiPlatformAdapter apiPlatformAdapter;
/**
* Returns the singleton instance of this class.
@@ -52,7 +61,28 @@ public class ApiFeatures {
/**
* Allow subclasses for mocking, but no direct instantiation.
*/
protected ApiFeatures() {}
protected ApiFeatures() {
if (getApiLevel() >= 5) {
try {
Class<?> clazz = Class.forName(
"com.google.android.apps.mytracks.util.EclairPlatformAdapter");
apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance();
} catch (Exception e) {
Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair"
+ " platform adapter", e);
}
}
if (apiPlatformAdapter == null) {
Log.i(MyTracksConstants.TAG,
"ApiFeatures: Using default platform adapter");
// Cupcake adapter is always supported, so it's safe to do static linkage.
apiPlatformAdapter = new CupcakePlatformAdapter();
}
}
public ApiPlatformAdapter getApiPlatformAdapter() {
return apiPlatformAdapter;
}
/**
* Returns whether cloud backup (a.k.a. Froyo backup) is available.
@@ -81,7 +111,7 @@ public class ApiFeatures {
public boolean hasModernSignalStrength() {
return getApiLevel() >= 7;
}
// Visible for testing.
protected int getApiLevel() {
return ANDROID_API_LEVEL;
@@ -0,0 +1,60 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.os.HandlerThread;
/**
* A set of methods that may be implemented in a platform specific way.
*
* @author Bartlomiej Niechwiej
*/
public interface ApiPlatformAdapter {
/**
* Puts the specified service into foreground.
*
* @param service the service to be put in foreground.
* @param notificationManager the notification manager used to post the given
* notification.
* @param id the ID of the notification, unique within the application.
* @param notification the notification to post.
*/
void startForeground(Service service, NotificationManager notificationManager,
int id, Notification notification);
/**
* Puts the given service into background.
* @param service the service to put into background.
* @param notificationManager the notification manager to user when removing
* notifications.
* @param id the ID of the notification to be remove, or -1 if the
* notification shouldn't be removed.
*/
void stopForeground(Service service, NotificationManager notificationManager,
int id);
/**
* Terminates the given handler thread.
*
* @param handlerThread the thread to be terminated.
* @return true whether the handler has been stopped or not.
*/
boolean stopHandlerThread(HandlerThread handlerThread);
}
@@ -0,0 +1,53 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.os.HandlerThread;
/**
* The Cupcake (API level 3) specific implementation of the
* {@link ApiPlatformAdapter}.
*
* @author Bartlomiej Niechwiej
*/
public class CupcakePlatformAdapter implements ApiPlatformAdapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
service.setForeground(true);
notificationManager.notify(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
service.setForeground(false);
if (id != -1) {
notificationManager.cancel(id);
}
}
@Override
public boolean stopHandlerThread(HandlerThread handlerThread) {
// Do nothing, as Cupcake doesn't provide quit().
return false;
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.os.HandlerThread;
/**
* The Eclair (API level 5) specific implementation of the
* {@link ApiPlatformAdapter}.
*
* @author Bartlomiej Niechwiej
*/
public class EclairPlatformAdapter implements ApiPlatformAdapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
service.startForeground(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
service.stopForeground(id != -1);
}
@Override
public boolean stopHandlerThread(HandlerThread handlerThread) {
return handlerThread.quit();
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import android.content.Context;
import android.test.AndroidTestCase;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Tests {@link DefaultTrackNameFactory}
*
* @author Matthew Simmons
*/
public class DefaultTrackNameFactoryTest extends AndroidTestCase {
/**
* A version of the factory which allows us to supply our own answer as to
* whether a timestamp-based track name should be used.
*/
private static class MockDefaultTrackNameFactory
extends DefaultTrackNameFactory {
private final boolean useTimestamp;
MockDefaultTrackNameFactory(Context context, boolean useTimestamp) {
super(context);
this.useTimestamp = useTimestamp;
}
@Override
protected boolean useTimestampTrackName() {
return useTimestamp;
}
}
private static final long TIMESTAMP = 1288213406000L;
public void testTimestampTrackName() {
DefaultTrackNameFactory factory =
new MockDefaultTrackNameFactory(getContext(), true);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
assertEquals(formatter.format(new Date(TIMESTAMP)),
factory.newTrackName(1, TIMESTAMP));
}
public void testIncrementingTrackName() {
DefaultTrackNameFactory factory =
new MockDefaultTrackNameFactory(getContext(), false);
assertEquals("Track 1", factory.newTrackName(1, TIMESTAMP));
}
}
@@ -0,0 +1,522 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import static com.google.android.testing.mocking.AndroidMock.capture;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import static com.google.android.testing.mocking.AndroidMock.same;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.test.AndroidTestCase;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import org.easymock.Capture;
/**
* Tests for {@link StatusAnnouncerTask}.
* WARNING: I'm not responsible if your eyes start bleeding while reading this
* code. You have been warned. It's still better than no test, though.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Use something other than our hardcoded value
private static final Locale DEFAULT_LOCALE = Locale.KOREAN;
private static final String ANNOUNCEMENT = "I can haz cheeseburger?";
private Locale oldDefaultLocale;
private StatusAnnouncerTask task;
private StringUtils stringUtils;
private StatusAnnouncerTask mockTask;
private Capture<OnInitListener> initListenerCapture;
private Capture<PhoneStateListener> phoneListenerCapture;
private TextToSpeechDelegate ttsDelegate;
private TextToSpeechInterface tts;
/**
* Mockable interface that we delegate TTS calls to.
*/
interface TextToSpeechInterface {
int addEarcon(String earcon, String packagename, int resourceId);
int addEarcon(String earcon, String filename);
int addSpeech(String text, String packagename, int resourceId);
int addSpeech(String text, String filename);
boolean areDefaultsEnforced();
String getDefaultEngine();
Locale getLanguage();
int isLanguageAvailable(Locale loc);
boolean isSpeaking();
int playEarcon(String earcon, int queueMode,
HashMap<String, String> params);
int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params);
int setEngineByPackageName(String enginePackageName);
int setLanguage(Locale loc);
int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener);
int setPitch(float pitch);
int setSpeechRate(float speechRate);
void shutdown();
int speak(String text, int queueMode, HashMap<String, String> params);
int stop();
int synthesizeToFile(String text, HashMap<String, String> params,
String filename);
}
/**
* Subclass of {@link TextToSpeech} which delegates calls to the interface
* above.
* The logic here is stupid and the author is ashamed of having to write it
* like this, but basically the issue is that TextToSpeech cannot be mocked
* without running its constructor, its constructor runs async operations
* which call other methods (and then if the methods are part of a mock we'd
* have to set a behaviour, but we can't 'cause the object hasn't been fully
* built yet).
* The logic is that calls made during the constructor (when tts is not yet
* set) will go up to the original class, but after tts is set we'll forward
* them all to the mock.
*/
private class TextToSpeechDelegate
extends TextToSpeech implements TextToSpeechInterface {
public TextToSpeechDelegate(Context context, OnInitListener listener) {
super(context, listener);
}
@Override
public int addEarcon(String earcon, String packagename, int resourceId) {
if (tts == null) {
return super.addEarcon(earcon, packagename, resourceId);
}
return tts.addEarcon(earcon, packagename, resourceId);
}
@Override
public int addEarcon(String earcon, String filename) {
if (tts == null) {
return super.addEarcon(earcon, filename);
}
return tts.addEarcon(earcon, filename);
}
@Override
public int addSpeech(String text, String packagename, int resourceId) {
if (tts == null) {
return super.addSpeech(text, packagename, resourceId);
}
return tts.addSpeech(text, packagename, resourceId);
}
@Override
public int addSpeech(String text, String filename) {
if (tts == null) {
return super.addSpeech(text, filename);
}
return tts.addSpeech(text, filename);
}
@Override
public Locale getLanguage() {
if (tts == null) {
return super.getLanguage();
}
return tts.getLanguage();
}
@Override
public int isLanguageAvailable(Locale loc) {
if (tts == null) {
return super.isLanguageAvailable(loc);
}
return tts.isLanguageAvailable(loc);
}
@Override
public boolean isSpeaking() {
if (tts == null) {
return super.isSpeaking();
}
return tts.isSpeaking();
}
@Override
public int playEarcon(String earcon, int queueMode,
HashMap<String, String> params) {
if (tts == null) {
return super.playEarcon(earcon, queueMode, params);
}
return tts.playEarcon(earcon, queueMode, params);
}
@Override
public int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params) {
if (tts == null) {
return super.playSilence(durationInMs, queueMode, params);
}
return tts.playSilence(durationInMs, queueMode, params);
}
@Override
public int setLanguage(Locale loc) {
if (tts == null) {
return super.setLanguage(loc);
}
return tts.setLanguage(loc);
}
@Override
public int setOnUtteranceCompletedListener(
OnUtteranceCompletedListener listener) {
if (tts == null) {
return super.setOnUtteranceCompletedListener(listener);
}
return tts.setOnUtteranceCompletedListener(listener);
}
@Override
public int setPitch(float pitch) {
if (tts == null) {
return super.setPitch(pitch);
}
return tts.setPitch(pitch);
}
@Override
public int setSpeechRate(float speechRate) {
if (tts == null) {
return super.setSpeechRate(speechRate);
}
return tts.setSpeechRate(speechRate);
}
@Override
public void shutdown() {
if (tts == null) {
super.shutdown();
return;
}
tts.shutdown();
}
@Override
public int speak(
String text, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.speak(text, queueMode, params);
}
return tts.speak(text, queueMode, params);
}
@Override
public int stop() {
if (tts == null) {
return super.stop();
}
return tts.stop();
}
@Override
public int synthesizeToFile(String text, HashMap<String, String> params,
String filename) {
if (tts == null) {
return super.synthesizeToFile(text, params, filename);
}
return tts.synthesizeToFile(text, params, filename);
}
}
@UsesMocks({
StatusAnnouncerTask.class,
StringUtils.class,
TrackRecordingService.class,
})
@Override
protected void setUp() throws Exception {
super.setUp();
oldDefaultLocale = Locale.getDefault();
Locale.setDefault(DEFAULT_LOCALE);
stringUtils = AndroidMock.createMock(StringUtils.class, getContext());
// Eww, the effort required just to mock TextToSpeech is insane
final AtomicBoolean listenerCalled = new AtomicBoolean();
OnInitListener blockingListener = new OnInitListener() {
@Override
public void onInit(int status) {
synchronized (this) {
listenerCalled.set(true);
notify();
}
}
};
ttsDelegate = new TextToSpeechDelegate(getContext(), blockingListener);
// Wait for all async operations done in the constructor to finish.
synchronized (blockingListener) {
while (!listenerCalled.get()) {
// Releases the synchronized lock until we're woken up.
blockingListener.wait();
}
}
// Phew, done, now we can start forwarding calls
tts = AndroidMock.createMock(TextToSpeechInterface.class);
initListenerCapture = new Capture<OnInitListener>();
phoneListenerCapture = new Capture<PhoneStateListener>();
// Create a partial forwarding mock
mockTask = AndroidMock.createMock(StatusAnnouncerTask.class,
getContext(), stringUtils);
task = new StatusAnnouncerTask(getContext(), stringUtils) {
@Override
protected TextToSpeech newTextToSpeech(Context ctx,
OnInitListener onInitListener) {
return mockTask.newTextToSpeech(ctx, onInitListener);
}
@Override
protected String getAnnouncement(TripStatistics stats) {
return mockTask.getAnnouncement(stats);
}
@Override
protected void listenToPhoneState(
PhoneStateListener listener, int events) {
mockTask.listenToPhoneState(listener, events);
}
};
}
@Override
protected void tearDown() {
Locale.setDefault(oldDefaultLocale);
}
public void testStart() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE))
.andStubReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setLanguage(DEFAULT_LOCALE))
.andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE))
.andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts, stringUtils);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
AndroidMock.verify(mockTask, tts, stringUtils);
}
public void testStart_languageNotSupported() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE))
.andStubReturn(TextToSpeech.LANG_NOT_SUPPORTED);
expect(tts.setLanguage(Locale.ENGLISH))
.andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE))
.andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts, stringUtils);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
AndroidMock.verify(mockTask, tts, stringUtils);
}
public void testStart_notReady() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
AndroidMock.replay(tts, stringUtils);
ttsInitListener.onInit(TextToSpeech.ERROR);
AndroidMock.verify(mockTask, tts, stringUtils);
}
public void testShutdown() {
// First, start
doStart();
AndroidMock.verify(mockTask);
AndroidMock.reset(mockTask);
// Then, shut down
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
mockTask.listenToPhoneState(
same(phoneListener), eq(PhoneStateListener.LISTEN_NONE));
tts.shutdown();
AndroidMock.replay(mockTask, tts, stringUtils);
task.shutdown();
AndroidMock.verify(mockTask, tts, stringUtils);
}
public void testRun() throws Exception {
// Expect service data calls
TripStatistics stats = new TripStatistics();
TrackRecordingService service =
AndroidMock.createMock(TrackRecordingService.class);
expect(service.getTripStatistics()).andStubReturn(stats);
// Expect announcement building call
expect(mockTask.getAnnouncement(same(stats))).andStubReturn(ANNOUNCEMENT);
// Put task in "ready" state
startTask(TextToSpeech.SUCCESS);
// Expect actual announcement call
expect(tts.speak(
eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
AndroidMock.<HashMap<String, String>>isNull()))
.andReturn(TextToSpeech.SUCCESS);
// Run the announcement
AndroidMock.replay(tts, stringUtils, service);
task.run(service);
AndroidMock.verify(mockTask, tts, stringUtils, service);
}
public void testRun_notReady() throws Exception {
TrackRecordingService service =
AndroidMock.createMock(TrackRecordingService.class);
// Put task in "not ready" state
startTask(TextToSpeech.ERROR);
// Run the announcement
AndroidMock.replay(tts, stringUtils, service);
task.run(service);
AndroidMock.verify(mockTask, tts, stringUtils, service);
}
public void testRun_duringCall() throws Exception {
TrackRecordingService service =
AndroidMock.createMock(TrackRecordingService.class);
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(false);
// Run the announcement
AndroidMock.replay(tts, stringUtils, service);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.run(service);
AndroidMock.verify(mockTask, tts, stringUtils, service);
}
public void testRun_ringWhileSpeaking() throws Exception {
TrackRecordingService service =
AndroidMock.createMock(TrackRecordingService.class);
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(true);
expect(tts.stop()).andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts, stringUtils, service);
// Update the state to ringing - this should stop the current announcement.
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement - this should do nothing.
task.run(service);
AndroidMock.verify(mockTask, tts, stringUtils, service);
}
public void testRun_whileRinging() throws Exception {
TrackRecordingService service =
AndroidMock.createMock(TrackRecordingService.class);
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(false);
// Run the announcement
AndroidMock.replay(tts, stringUtils, service);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.run(service);
AndroidMock.verify(mockTask, tts, stringUtils, service);
}
public void testRun_noService() throws Exception {
startTask(TextToSpeech.SUCCESS);
// Run the announcement
AndroidMock.replay(tts, stringUtils);
task.run(null);
AndroidMock.verify(mockTask, tts, stringUtils);
}
public void testRun_noStats() throws Exception {
// Expect service data calls
TrackRecordingService service =
AndroidMock.createMock(TrackRecordingService.class);
expect(service.getTripStatistics()).andStubReturn(null);
startTask(TextToSpeech.SUCCESS);
// Run the announcement
AndroidMock.replay(tts, stringUtils, service);
task.run(service);
AndroidMock.verify(mockTask, tts, stringUtils, service);
}
private void startTask(int state) {
AndroidMock.resetToNice(tts, stringUtils);
AndroidMock.replay(tts, stringUtils);
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
ttsInitListener.onInit(state);
AndroidMock.resetToDefault(tts, stringUtils);
}
private void doStart() {
mockTask.listenToPhoneState(capture(phoneListenerCapture),
eq(PhoneStateListener.LISTEN_CALL_STATE));
expect(mockTask.newTextToSpeech(
same(getContext()), capture(initListenerCapture)))
.andStubReturn(ttsDelegate);
AndroidMock.replay(mockTask);
task.start();
}
}
@@ -15,12 +15,33 @@
*/
package com.google.android.apps.mytracks.services;
import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.content.MyTracksProvider;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.location.Location;
import android.os.IBinder;
import android.test.RenamingDelegatingContext;
import android.test.ServiceTestCase;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import java.util.List;
/**
* Tests for the MyTracks track recording service.
*
@@ -28,24 +49,513 @@ import android.test.suitebuilder.annotation.SmallTest;
*/
public class TrackRecordingServiceTest
extends ServiceTestCase<TrackRecordingService> {
private Context context;
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
public TrackRecordingServiceTest() {
super(TrackRecordingService.class);
}
/**
* A context wrapper with the user provided {@link ContentResolver}.
*/
private static class MockContext extends ContextWrapper {
private final ContentResolver contentResolver;
public MockContext(ContentResolver contentResolver, Context base) {
super(base);
this.contentResolver = contentResolver;
}
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
}
/**
* A mock class that forces API level < 5 to make sure we can workaround a bug
* in ServiceTestCase (throwing a NPE).
* See http://code.google.com/p/android/issues/detail?id=12122 for more
* details.
*/
private static class MockApiFeatures extends ApiFeatures {
@Override
protected int getApiLevel() {
return 4;
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
ApiFeatures.injectInstance(new MockApiFeatures());
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
context = new MockContext(mockContentResolver, targetContext);
MyTracksProvider provider = new MyTracksProvider();
provider.attachInfo(context, null);
mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider);
setContext(context);
providerUtils = MyTracksProviderUtils.Factory.get(context);
sharedPreferences = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
// Disable auto resume by default.
updateAutoResumePrefs(0, -1);
// No recording track.
Editor editor = sharedPreferences.edit();
editor.putLong(context.getString(R.string.recording_track_key), -1);
editor.commit();
}
@SmallTest
public void testStartable() {
Intent startIntent = new Intent();
startIntent.setClass(getContext(), TrackRecordingService.class);
startService(startIntent);
startService(createStartIntent());
assertNotNull(getService());
}
@MediumTest
public void testBindable() {
Intent startIntent = new Intent();
startIntent.setClass(getContext(), TrackRecordingService.class);
IBinder service = bindService(startIntent);
IBinder service = bindService(createStartIntent());
assertNotNull(service);
}
@MediumTest
public void testResumeAfterReboot_shouldResume() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(123, System.currentTimeMillis(), true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(0, 10);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
// We expect to resume the previous track.
assertTrue(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(123, service.getRecordingTrackId());
}
@MediumTest
public void testResumeAfterReboot_simulateReboot() throws Exception {
updateAutoResumePrefs(0, 10);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// Simulate recording a track.
long id = service.startNewTrack();
assertTrue(service.isRecording());
assertEquals(id, service.getRecordingTrackId());
shutdownService();
assertEquals(id, sharedPreferences.getLong(
context.getString(R.string.recording_track_key), -1));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
// TODO: shutdownService() has a bug and doesn't set mServiceCreated
// to false, thus preventing from a second call to onCreate().
// Report the bug to Android team. Until then, the following check
// must be commented out.
// assertTrue(getService().isRecording());
}
@MediumTest
public void testResumeAfterReboot_noRecordingTrack() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(123, System.currentTimeMillis(), false);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(0, 10);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
// We don't expect to resume the previous track, because it was stopped.
assertFalse(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago.
createDummyTrack(123, System.currentTimeMillis() - 20 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(0, 10);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
// We don't expect to resume the previous track, because it has expired.
assertFalse(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testResumeAfterReboot_tooManyAttempts() throws Exception {
// Insert a dummy track.
createDummyTrack(123, System.currentTimeMillis(), true);
// Set the number of attempts to max.
updateAutoResumePrefs(
TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, 10);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
// We don't expect to resume the previous track, because there were already
// too many attempts.
assertFalse(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testRecording_noTracks() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent());
// Test if we start in no-recording mode by default.
assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testRecording_oldTracks() throws Exception {
createDummyTrack(123, -1, false);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testRecording_orphanedRecordingTrack() throws Exception {
// Just set recording track to a bogus value.
setRecordingTrack(256);
// Make sure that the service will not start recording and will clear
// the bogus track.
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testStartNewTrack_noRecording() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
long id = service.startNewTrack();
assertTrue(id >= 0);
assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id);
assertNotNull(track);
assertEquals(id, track.getId());
assertEquals(id, sharedPreferences.getLong(
context.getString(R.string.recording_track_key), -1));
assertEquals(id, service.getRecordingTrackId());
}
@MediumTest
public void testStartNewTrack_alreadyRecording() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
try {
service.startNewTrack();
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
assertEquals(123, sharedPreferences.getLong(
context.getString(R.string.recording_track_key), 0));
assertEquals(123, service.getRecordingTrackId());
}
@MediumTest
public void testEndCurrentTrack_alreadyRecording() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
// End the current track.
service.endCurrentTrack();
assertFalse(service.isRecording());
assertEquals(-1, sharedPreferences.getLong(
context.getString(R.string.recording_track_key), 0));
assertEquals(-1, service.getRecordingTrackId());
}
@MediumTest
public void testEndCurrentTrack_noRecording() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// End the current track.
try {
service.endCurrentTrack();
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
assertEquals(-1, sharedPreferences.getLong(
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());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// Start a track.
long id = service.startNewTrack();
assertTrue(id >= 0);
assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id);
assertNotNull(track);
assertEquals(id, track.getId());
assertEquals(id, sharedPreferences.getLong(
context.getString(R.string.recording_track_key), -1));
assertEquals(id, service.getRecordingTrackId());
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId());
track = providerUtils.getTrack(id);
assertNotNull(track);
assertEquals(id, track.getId());
TripStatistics tripStatistics = track.getStatistics();
assertNotNull(tripStatistics);
assertTrue(tripStatistics.getStartTime() > 0);
assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime());
}
@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());
Location loc = new Location("gps");
try {
service.insertStatisticsMarker(loc);
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
}
@MediumTest
public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
Location loc = new Location("gps");
assertEquals(1, service.insertStatisticsMarker(loc));
assertEquals(2, service.insertStatisticsMarker(loc));
// TODO: Add more checks.
}
@MediumTest
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
Location loc = new Location("gps");
Waypoint waypoint = new Waypoint();
waypoint.setId(1);
waypoint.setLocation(loc);
try {
service.insertWaypointMarker(waypoint);
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
}
@MediumTest
public void testInsertWaypointMarker_invalidWaypoint() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
Waypoint waypoint = new Waypoint();
assertEquals(-1, service.insertWaypointMarker(waypoint));
}
@MediumTest
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
Location loc = new Location("gps");
Waypoint waypoint = new Waypoint();
waypoint.setId(1);
waypoint.setLocation(loc);
assertEquals(1, service.insertWaypointMarker(waypoint));
}
private ITrackRecordingService bindAndGetService(Intent intent) {
ITrackRecordingService service = ITrackRecordingService.Stub.asInterface(
bindService(intent));
assertNotNull(service);
return service;
}
private Track createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track();
dummyTrack.setId(id);
dummyTrack.setName("Dummy Track");
TripStatistics tripStatistics = new TripStatistics();
tripStatistics.setStopTime(stopTime);
dummyTrack.setStatistics(tripStatistics);
addTrack(dummyTrack, isRecording);
return dummyTrack;
}
private void updateAutoResumePrefs(int attempts, int timeoutMins) {
Editor editor = sharedPreferences.edit();
editor.putInt(context.getString(
R.string.auto_resume_track_current_retry_key), attempts);
editor.putInt(context.getString(
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);
editor.commit();
}
}
@@ -20,13 +20,11 @@ public class TripStatisticsBuilderTest extends TestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
builder = new TripStatisticsBuilder();
builder.resume();
builder = new TripStatisticsBuilder(System.currentTimeMillis());
}
public void testAddLocationSimple() throws Exception {
builder = new TripStatisticsBuilder();
builder.resumeAt(1000);
builder = new TripStatisticsBuilder(1000);
TripStatistics stats = builder.getStatistics();
assertEquals(0.0, builder.getSmoothedElevation());
@@ -84,8 +82,7 @@ public class TripStatisticsBuilderTest extends TestCase {
*/
public void testElevationSimple() throws Exception {
for (double elevation = 0; elevation < 1000; elevation += 10) {
builder = new TripStatisticsBuilder();
builder.resume();
builder = new TripStatisticsBuilder(System.currentTimeMillis());
for (int j = 0; j < 100; j++) {
assertEquals(0.0, builder.updateElevation(elevation));
assertEquals(elevation, builder.getSmoothedElevation());
@@ -75,4 +75,8 @@ public class ApiFeaturesTest extends TestCase {
features.setApiLevel(8);
assertTrue(features.hasModernSignalStrength());
}
public void testGetApiPlatformAdapter() {
assertNotNull(features.getApiPlatformAdapter());
}
}