Merging to head.

This commit is contained in:
Rodrigo Damazio
2011-04-12 01:42:27 -03:00
parent 3501fff7fc
commit 28a5c5b343
40 changed files with 1701 additions and 996 deletions
@@ -24,7 +24,7 @@ import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
@@ -21,7 +21,7 @@ import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType;
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.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect;
@@ -43,8 +43,8 @@ import com.google.android.apps.mytracks.io.sendtogoogle.SendDialog;
import com.google.android.apps.mytracks.io.sendtogoogle.SendResult;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
@@ -19,8 +19,8 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.maps.mytracks.R;
@@ -266,9 +266,6 @@ public class SettingsActivity extends PreferenceActivity {
final ListPreference minRequiredAccuracy =
(ListPreference) findPreference(
getString(R.string.min_required_accuracy_key));
final ListPreference splitFrequency =
(ListPreference) findPreference(
getString(R.string.split_frequency_key));
minRecordingDistance.setEntries(isMetric
? R.array.min_recording_distance_options
@@ -279,11 +276,20 @@ public class SettingsActivity extends PreferenceActivity {
minRequiredAccuracy.setEntries(isMetric
? R.array.min_required_accuracy_options
: R.array.min_required_accuracy_options_ft);
splitFrequency.setEntries(isMetric
? R.array.split_frequency_options
: R.array.split_frequency_options_ft);
setTaskOptions(isMetric, R.string.announcement_frequency_key);
setTaskOptions(isMetric, R.string.split_frequency_key);
}
/**
* Set the user visible options for a periodic task.
*/
private void setTaskOptions(boolean isMetric, int listId) {
final ListPreference taskFrequency =
(ListPreference) findPreference(getString(listId));
taskFrequency.setEntries(isMetric
? R.array.task_frequency_options
: R.array.task_frequency_options_ft);
}
/**
* Configures preference actions related to bluetooth.
@@ -20,7 +20,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
@@ -798,7 +798,7 @@ public class TrackDataHub {
if (RECORDING_TRACK_KEY.equals(key)) {
recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1);
} else if (MIN_REQUIRED_ACCURACY_KEY.equals(key)) {
minRequiredAccuracy = preferences.getInt(RECORDING_TRACK_KEY,
minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY,
Constants.DEFAULT_MIN_REQUIRED_ACCURACY);
} else if (METRIC_UNITS_KEY.equals(key)) {
useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true);
@@ -1,12 +1,12 @@
/*
* Copyright 2008 Google Inc.
*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,332 +15,54 @@
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksLocation;
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.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/**
* The class which exports tracks to the SD card.
* This writer is format-neutral - it handles creating the output file
* and reading the track to be exported, but requires an instance of
* Implementations of this class export tracks to the SD card. This class is
* intended to be format-neutral - it handles creating the output file and
* reading the track to be exported, but requires an instance of
* {@link TrackFormatWriter} to actually format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class TrackWriter {
private final Context context;
private final MyTracksProviderUtils providerUtils;
private final Track track;
private final TrackFormatWriter writer;
private final FileUtils fileUtils;
private Runnable onCompletion = null;
private boolean success = false;
private int errorMessage = -1;
private File directory = null;
private File file = null;
public TrackWriter(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
this.track = track;
this.writer = writer;
this.fileUtils = new FileUtils();
}
public interface TrackWriter {
/**
* Sets a completion callback.
*
* @param onCompletion Runnable that will be executed when finished
*/
public void setOnCompletion(Runnable onCompletion) {
this.onCompletion = onCompletion;
}
void setOnCompletion(Runnable onCompletion);
/**
* Sets a custom directory where the file will be written.
*/
public void setDirectory(File directory) {
this.directory = directory;
}
void setDirectory(File directory);
public String getAbsolutePath() {
return file.getAbsolutePath();
}
/**
* Returns the absolute path to the file which was created.
*/
String getAbsolutePath();
/**
* Writes the given track id to the SD card.
* This is non-blocking.
*/
public void writeTrackAsync() {
Thread t = new Thread() {
@Override
public void run() {
writeTrack();
}
};
t.start();
}
void writeTrackAsync();
/**
* Writes the given track id to the SD card.
* This is blocking.
*/
public void writeTrack() {
// Open the input and output
success = false;
errorMessage = R.string.error_track_does_not_exist;
if (track != null) {
if (openFile()) {
writeDocument();
}
}
finished();
}
public int getErrorMessage() {
return errorMessage;
}
public boolean wasSuccess() {
return success;
}
/*
* Helper methods:
* ===============
*/
private void finished() {
if (onCompletion != null) {
runOnUiThread(onCompletion);
return;
}
}
void writeTrack();
/**
* Runs the given runnable in the UI thread.
* Returns true if the write completed successfully.
*/
protected void runOnUiThread(Runnable runnable) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(runnable);
}
}
boolean wasSuccess();
/**
* Opens the file and prepares the format writer for it.
*
* @return true on success, false otherwise (and errorMessage is set)
* Returns the error message (if any) generated by a writer failure.
*/
protected boolean openFile() {
if (!canWriteFile()) {
return false;
}
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = fileUtils.buildUniqueFileName(
directory, track.getName(), writer.getExtension());
if (fileName == null) {
Log.e(Constants.TAG,
"Unable to get a unique filename for " + fileName);
return false;
}
Log.i(Constants.TAG, "Writing track to: " + fileName);
try {
writer.prepare(track, newOutputStream(fileName));
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Failed to open output file.", e);
errorMessage = R.string.io_write_failed;
return false;
}
return true;
}
/**
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
if (directory == null) {
String dirName =
fileUtils.buildExternalDirectoryPath(writer.getExtension());
directory = newFile(dirName);
}
if (!fileUtils.isSdCardAvailable()) {
Log.i(Constants.TAG, "Could not find SD card.");
errorMessage = R.string.io_no_external_storage_found;
return false;
}
if (!fileUtils.ensureDirectoryExists(directory)) {
Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.io_create_dir_failed;
return false;
}
return true;
}
/**
* Creates a new output stream to write to the given filename.
*
* @throws FileNotFoundException if the file could't be created
*/
protected OutputStream newOutputStream(String fileName)
throws FileNotFoundException {
file = new File(directory, fileName);
return new FileOutputStream(file);
}
/**
* Creates a new file object for the given path.
*/
protected File newFile(String path) {
return new File(path);
}
/**
* Writes the waypoints for the given track.
*
* @param trackId the ID of the track to write waypoints for
*/
private void writeWaypoints(long trackId) {
// TODO: Stream through he waypoints in chunks.
// I am leaving the number of waypoints very high which should not be a
// problem because we don't try to load them into objects all at the
// same time.
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Yes, this will skip the 1st way point and that is intentional
// as the 1st points holds the stats for the current/last segment.
while (cursor.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
/**
* Does the actual work of writing the track to the now open file.
*/
void writeDocument() {
Log.d(Constants.TAG, "Started writing track.");
writer.writeHeader();
writeWaypoints(track.getId());
writeLocations();
writer.writeFooter();
writer.close();
success = true;
Log.d(Constants.TAG, "Done writing track.");
errorMessage = R.string.io_write_finished;
}
private void writeLocations() {
boolean wroteFirst = false;
boolean segmentOpen = false;
boolean isLastValid = false;
class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new MyTracksLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tmpLoc = lastLocation;
lastLocation = currentLocation;
currentLocation = tmpLoc;
if (currentLocation != null) {
currentLocation.reset();
}
}
};
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
locationFactory);
try {
if (!it.hasNext()) {
Log.w(Constants.TAG, "Unable to get any points to write");
return;
}
while (it.hasNext()) {
Location loc = it.next();
boolean isValid = LocationUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(locationFactory.lastLocation);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(locationFactory.lastLocation);
}
// Write the current point
writer.writeLocation(loc);
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
locationFactory.swapLocations();
isLastValid = isValid;
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(locationFactory.lastLocation);
}
} finally {
it.close();
}
}
int getErrorMessage();
}
@@ -106,7 +106,7 @@ public class TrackWriterFactory {
MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context);
return new TrackWriter(context, providerUtils, track, writer);
return new TrackWriterImpl(context, providerUtils, track, writer);
}
private TrackWriterFactory() { }
@@ -0,0 +1,336 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/**
* This class exports tracks to the SD card. It is intended to be format-
* neutral -- it handles creating the output file and reading the track to be
* exported, but requires an instance of {@link TrackFormatWriter} to actually
* format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class TrackWriterImpl implements TrackWriter {
private final Context context;
private final MyTracksProviderUtils providerUtils;
private final Track track;
private final TrackFormatWriter writer;
private final FileUtils fileUtils;
private Runnable onCompletion = null;
private boolean success = false;
private int errorMessage = -1;
private File directory = null;
private File file = null;
TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
this.track = track;
this.writer = writer;
this.fileUtils = new FileUtils();
}
@Override
public void setOnCompletion(Runnable onCompletion) {
this.onCompletion = onCompletion;
}
@Override
public void setDirectory(File directory) {
this.directory = directory;
}
@Override
public String getAbsolutePath() {
return file.getAbsolutePath();
}
@Override
public void writeTrackAsync() {
Thread t = new Thread() {
@Override
public void run() {
writeTrack();
}
};
t.start();
}
@Override
public void writeTrack() {
// Open the input and output
success = false;
errorMessage = R.string.error_track_does_not_exist;
if (track != null) {
if (openFile()) {
writeDocument();
}
}
finished();
}
@Override
public int getErrorMessage() {
return errorMessage;
}
@Override
public boolean wasSuccess() {
return success;
}
/*
* Helper methods:
* ===============
*/
private void finished() {
if (onCompletion != null) {
runOnUiThread(onCompletion);
return;
}
}
/**
* Runs the given runnable in the UI thread.
*/
protected void runOnUiThread(Runnable runnable) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(runnable);
}
}
/**
* Opens the file and prepares the format writer for it.
*
* @return true on success, false otherwise (and errorMessage is set)
*/
protected boolean openFile() {
if (!canWriteFile()) {
return false;
}
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = fileUtils.buildUniqueFileName(
directory, track.getName(), writer.getExtension());
if (fileName == null) {
Log.e(Constants.TAG,
"Unable to get a unique filename for " + fileName);
return false;
}
Log.i(Constants.TAG, "Writing track to: " + fileName);
try {
writer.prepare(track, newOutputStream(fileName));
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Failed to open output file.", e);
errorMessage = R.string.io_write_failed;
return false;
}
return true;
}
/**
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
if (directory == null) {
String dirName =
fileUtils.buildExternalDirectoryPath(writer.getExtension());
directory = newFile(dirName);
}
if (!fileUtils.isSdCardAvailable()) {
Log.i(Constants.TAG, "Could not find SD card.");
errorMessage = R.string.io_no_external_storage_found;
return false;
}
if (!fileUtils.ensureDirectoryExists(directory)) {
Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.io_create_dir_failed;
return false;
}
return true;
}
/**
* Creates a new output stream to write to the given filename.
*
* @throws FileNotFoundException if the file could't be created
*/
protected OutputStream newOutputStream(String fileName)
throws FileNotFoundException {
file = new File(directory, fileName);
return new FileOutputStream(file);
}
/**
* Creates a new file object for the given path.
*/
protected File newFile(String path) {
return new File(path);
}
/**
* Writes the waypoints for the given track.
*
* @param trackId the ID of the track to write waypoints for
*/
private void writeWaypoints(long trackId) {
// TODO: Stream through he waypoints in chunks.
// I am leaving the number of waypoints very high which should not be a
// problem because we don't try to load them into objects all at the
// same time.
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Yes, this will skip the 1st way point and that is intentional
// as the 1st points holds the stats for the current/last segment.
while (cursor.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
/**
* Does the actual work of writing the track to the now open file.
*/
void writeDocument() {
Log.d(Constants.TAG, "Started writing track.");
writer.writeHeader();
writeWaypoints(track.getId());
writeLocations();
writer.writeFooter();
writer.close();
success = true;
Log.d(Constants.TAG, "Done writing track.");
errorMessage = R.string.io_write_finished;
}
private void writeLocations() {
boolean wroteFirst = false;
boolean segmentOpen = false;
boolean isLastValid = false;
class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new MyTracksLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tmpLoc = lastLocation;
lastLocation = currentLocation;
currentLocation = tmpLoc;
if (currentLocation != null) {
currentLocation.reset();
}
}
};
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
locationFactory);
try {
if (!it.hasNext()) {
Log.w(Constants.TAG, "Unable to get any points to write");
return;
}
while (it.hasNext()) {
Location loc = it.next();
boolean isValid = LocationUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(locationFactory.lastLocation);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(locationFactory.lastLocation);
}
// Write the current point
writer.writeLocation(loc);
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
locationFactory.swapLocations();
isLastValid = isValid;
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(locationFactory.lastLocation);
}
} finally {
it.close();
}
}
}
@@ -156,11 +156,11 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
}
}
if (key == null || key.equals(splitFrequencyKey)) {
service.getSplitManager().setSplitFrequency(
service.setSplitFrequency(
sharedPreferences.getInt(splitFrequencyKey, 0));
}
if (key == null || key.equals(metricUnitsKey)) {
service.getSplitManager().setMetricUnits(
service.setMetricUnits(
sharedPreferences.getBoolean(metricUnitsKey, true));
}
}
@@ -1,160 +0,0 @@
/*
* 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.Constants;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
/**
* This class manages inserting time or distance splits.
*
* @author Sandor Dornbush
*/
public class SplitManager {
/**
* The frequency of splits.
*/
private int splitFrequency = 0;
/**
* The next distance when a split should be taken.
*/
private double nextSplitDistance = 0;
/**
* Splits executer.
*/
private PeriodicTaskExecuter splitExecuter = null;
private boolean metricUnits;
private final TrackRecordingService service;
public SplitManager(TrackRecordingService service) {
this.service = service;
}
/**
* Restores the manager.
*/
public void restore() {
if ((splitFrequency > 0) && (splitExecuter != null)) {
splitExecuter.scheduleTask(splitFrequency * 60000);
}
calculateNextSplit();
}
/**
* Shuts down the manager.
*/
public void shutdown() {
if (splitExecuter != null) {
splitExecuter.shutdown();
}
}
/**
* Calculates the next distance that a split should be inserted at.
*/
public void calculateNextSplit() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (splitFrequency >= 0) {
nextSplitDistance = Double.MAX_VALUE;
Log.d(Constants.TAG,
"SplitManager: Distance splits disabled.");
return;
}
double dist = service.getTripStatistics().getTotalDistance() / 1000;
if (!metricUnits) {
dist *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative.
int index = (int) (dist / splitFrequency);
index -= 1;
nextSplitDistance = splitFrequency * index;
Log.d(Constants.TAG,
"SplitManager: Next split distance: " + nextSplitDistance);
}
/**
* Updates split manager with new trip statistics.
*/
public void updateSplits() {
if (this.splitFrequency >= 0) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() / 1000.0;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > this.nextSplitDistance) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
calculateNextSplit();
}
}
/**
* Sets the split frequency.
* < 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off splits
* > 0 Use the value as a time in minutes
* @param splitFrequency The frequency in time or distance
*/
public void setSplitFrequency(int splitFrequency) {
Log.d(Constants.TAG,
"setSplitFrequency: splitFrequency = " + splitFrequency);
this.splitFrequency = splitFrequency;
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (splitFrequency < 1) {
if (splitExecuter != null) {
splitExecuter.shutdown();
splitExecuter = null;
}
}
if (splitFrequency > 0) {
if (splitExecuter == null) {
TimeSplitTask splitter = new TimeSplitTask();
splitExecuter = new PeriodicTaskExecuter(splitter, service);
}
splitExecuter.scheduleTask(splitFrequency * 60000);
} else {
// For distance based splits.
calculateNextSplit();
}
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextSplit();
}
}
@@ -1,87 +0,0 @@
/*
* 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.Constants;
import android.util.Log;
/**
* This class manages a period task executer.
*
* @author Sandor Dornbush
*/
public class TaskExecuterManager {
private int frequency;
private final PeriodicTask task;
private PeriodicTaskExecuter executer;
public TaskExecuterManager(int frequency, PeriodicTask task,
TrackRecordingService service) {
this.task = task;
setFrequency(frequency, service);
}
public int getFrequency() {
return frequency;
}
/**
* Sets the frequency that the task should be run at.
* If needed the task will be scheduled.
*
* @param frequency The frequency in minutes for the task to run
* @param service The service to run the task on
*/
public void setFrequency(int frequency, TrackRecordingService service) {
this.frequency = frequency;
Log.i(Constants.TAG, "Frequency set to " + frequency
+ " for task " + task.getClass().getSimpleName());
if (frequency == -1) {
if (executer != null) {
executer.shutdown();
executer = null;
Log.i(Constants.TAG,
"Shut down service: " + task.getClass().getSimpleName());
}
} else {
if (executer == null) {
executer = new PeriodicTaskExecuter(task, service);
}
executer.scheduleTask(frequency * 60000);
}
}
/**
* Restores the task at the current frequency.
*/
public void restore() {
if (frequency > 0) {
executer.scheduleTask(frequency * 60000);
}
}
/**
* Shuts down this executer.
*/
public void shutdown() {
if (executer != null) {
executer.shutdown();
}
}
}
@@ -31,6 +31,9 @@ import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory;
import com.google.android.apps.mytracks.services.tasks.PeriodicTaskExecutor;
import com.google.android.apps.mytracks.services.tasks.SplitTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
@@ -111,10 +114,10 @@ public class TrackRecordingService extends Service implements LocationListener {
private double length;
/**
* Status announcer executer.
* Status announcer executor.
*/
private PeriodicTaskExecuter announcementExecuter;
private SplitManager splitManager;
private PeriodicTaskExecutor announcementExecutor;
private PeriodicTaskExecutor splitExecutor;
private SensorManager sensorManager;
@@ -185,11 +188,9 @@ public class TrackRecordingService extends Service implements LocationListener {
private Location lastValidLocation;
/**
* The frequency of status announcements.
* A service to run tasks outside of the main thread.
*/
private int announcementFrequency = -1;
private ExecutorService executerServce;
private ExecutorService executorService;
/*
* Utility functions
@@ -273,7 +274,8 @@ public class TrackRecordingService extends Service implements LocationListener {
"Caught SQLiteException: " + e.getMessage(), e);
return false;
}
splitManager.updateSplits();
announcementExecutor.update();
splitExecutor.update();
return true;
}
@@ -425,9 +427,7 @@ public class TrackRecordingService extends Service implements LocationListener {
TripStatistics stats = track.getStatistics();
statsBuilder = new TripStatisticsBuilder(stats.getStartTime());
statsBuilder.setMinRecordingDistance(minRecordingDistance);
setUpAnnouncer();
splitManager.restore();
length = 0;
lastValidLocation = null;
@@ -475,7 +475,8 @@ public class TrackRecordingService extends Service implements LocationListener {
}
}
splitManager.calculateNextSplit();
announcementExecutor.restore();
splitExecutor.restore();
}
/*
@@ -483,7 +484,7 @@ public class TrackRecordingService extends Service implements LocationListener {
*/
@Override
public void onLocationChanged(final Location location) {
this.executerServce.submit(
executorService.submit(
new Runnable() {
@Override
public void run() {
@@ -662,8 +663,8 @@ public class TrackRecordingService extends Service implements LocationListener {
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
splitManager = new SplitManager(this);
setUpTaskExecutors();
prefManager = new PreferenceManager(this);
registerLocationListener();
@@ -688,49 +689,29 @@ public class TrackRecordingService extends Service implements LocationListener {
prefManager.setRecordingTrack(recordingTrackId = -1);
}
showNotification();
executerServce = Executors.newSingleThreadExecutor();
executorService = Executors.newSingleThreadExecutor();
}
/**
* Creates an {@link Executer} and schedules {@class SafeStatusAnnouncerTask}.
* The announcer requires a TTS service and user should have enabled
* the announcements, otherwise this method is no-op.
* Creates the periodic task executors.
*/
private void setUpAnnouncer() {
Log.d(TAG, "TrackRecordingService.setUpAnnouncer: "
+ announcementExecuter);
if (announcementFrequency != -1 && recordingTrackId != -1) {
handler.post(new Runnable() {
@Override
public void run() {
if (announcementExecuter == null) {
StatusAnnouncerFactory statusAnnouncerFactory =
new StatusAnnouncerFactory(ApiFeatures.getInstance());
PeriodicTask announcer = statusAnnouncerFactory.create(
TrackRecordingService.this);
if (announcer == null) {
return;
}
// TODO: Either use TaskExecuterManager everywhere, or get rid of it
announcementExecuter = new PeriodicTaskExecuter(announcer,
TrackRecordingService.this);
}
announcementExecuter.scheduleTask(announcementFrequency * 60000);
}
});
}
private void setUpTaskExecutors() {
announcementExecutor = new PeriodicTaskExecutor(
this, new StatusAnnouncerFactory(ApiFeatures.getInstance()));
splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
}
private void shutdownAnnouncer() {
Log.d(TAG, "TrackRecordingService.shutdownAnnouncer: "
+ announcementExecuter);
if (announcementExecuter != null) {
try {
announcementExecuter.shutdown();
} finally {
announcementExecuter = null;
}
private void shutdownTaskExecutors() {
Log.d(TAG, "TrackRecordingService.shutdownExecuters");
try {
announcementExecutor.shutdown();
} finally {
announcementExecutor = null;
}
try {
splitExecutor.shutdown();
} finally {
splitExecutor = null;
}
}
@@ -747,9 +728,7 @@ public class TrackRecordingService extends Service implements LocationListener {
timer.cancel();
timer.purge();
unregisterLocationListener();
shutdownAnnouncer();
splitManager.shutdown();
splitManager = null;
shutdownTaskExecutors();
if (sensorManager != null) {
sensorManager.shutdown();
sensorManager = null;
@@ -1104,11 +1083,9 @@ public class TrackRecordingService extends Service implements LocationListener {
waypointStatsBuilder = new TripStatisticsBuilder(startTime);
waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance);
currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
setUpAnnouncer();
length = 0;
showNotification();
registerLocationListener();
splitManager.restore();
sensorManager = SensorManagerFactory.getSensorManager(this);
if (sensorManager != null) {
sensorManager.onStartTrack();
@@ -1123,6 +1100,8 @@ public class TrackRecordingService extends Service implements LocationListener {
// Notify the world that we're now recording.
sendTrackBroadcast(
R.string.track_started_broadcast_action, recordingTrackId);
announcementExecutor.restore();
splitExecutor.restore();
return recordingTrackId;
}
@@ -1133,7 +1112,8 @@ public class TrackRecordingService extends Service implements LocationListener {
throw new IllegalStateException("No recording track in progress!");
}
shutdownAnnouncer();
announcementExecutor.shutdown();
splitExecutor.shutdown();
isRecording = false;
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
if (recordingTrack != null) {
@@ -1193,19 +1173,6 @@ public class TrackRecordingService extends Service implements LocationListener {
this.recordingTrackId = recordingTrackId;
}
public int getAnnouncementFrequency() {
return announcementFrequency;
}
public void setAnnouncementFrequency(int announcementFrequency) {
this.announcementFrequency = announcementFrequency;
if (announcementFrequency == -1) {
shutdownAnnouncer();
} else {
setUpAnnouncer();
}
}
public int getMaxRecordingDistance() {
return maxRecordingDistance;
}
@@ -1251,7 +1218,16 @@ public class TrackRecordingService extends Service implements LocationListener {
this.autoResumeTrackTimeout = autoResumeTrackTimeout;
}
public SplitManager getSplitManager() {
return splitManager;
public void setAnnouncementFrequency(int announcementFrequency) {
announcementExecutor.setTaskFrequency(announcementFrequency);
}
public void setSplitFrequency(int frequency) {
splitExecutor.setTaskFrequency(frequency);
}
public void setMetricUnits(boolean metric) {
announcementExecutor.setMetricUnits(metric);
splitExecutor.setMetricUnits(metric);
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package com.google.android.apps.mytracks.services;
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
@@ -26,6 +26,7 @@ import android.util.Log;
import java.util.HashMap;
/**
* This class will periodically announce the user's trip statistics for Froyo and future handsets.
* This class will request and release audio focus.
@@ -14,7 +14,9 @@
* the License.
*/
package com.google.android.apps.mytracks.services;
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This is interface for a task that will be executed on some schedule.
@@ -0,0 +1,189 @@
/*
* 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.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
/**
* Execute a task on a time or distance schedule.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecutor {
/**
* The frequency of the task.
* A value greater than zero is a frequency in time.
* A value less than zero is considered a frequency in distance.
*/
private int taskFrequency = 0;
/**
* The next distance when the task should execute.
*/
private double nextTaskDistance = 0;
/**
* Time based executor.
*/
private TimerTaskExecutor timerExecutor = null;
private boolean metricUnits;
private final TrackRecordingService service;
private final PeriodicTaskFactory factory;
private PeriodicTask task;
public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) {
this.service = service;
this.factory = factory;
}
/**
* Restores the manager.
*/
public void restore() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (!isTimeFrequency()) {
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
}
}
if (taskFrequency == 0) {
return;
}
// Try to make the task.
task = factory.create(service);
// Returning null is ok.
if (task == null) {
return;
}
task.start();
if (isTimeFrequency()) {
if (timerExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service);
}
timerExecutor.scheduleTask(taskFrequency * 60000);
} else {
// For distance based splits.
calculateNextTaskDistance();
}
}
/**
* Shuts down the manager.
*/
public void shutdown() {
if (task != null) {
task.shutdown();
task = null;
}
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
}
}
/**
* Calculates the next distance when the task should execute.
*/
void calculateNextTaskDistance() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording() || task == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
double distance = service.getTripStatistics().getTotalDistance() / 1000;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency);
index -= 1;
nextTaskDistance = taskFrequency * index;
Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance);
}
/**
* Updates executer with new trip statistics.
*/
public void update() {
if (!isDistanceFrequency() || task == null) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() / 1000.0;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) {
task.run(service);
calculateNextTaskDistance();
}
}
private boolean isTimeFrequency() {
return taskFrequency > 0;
}
private boolean isDistanceFrequency() {
return taskFrequency < 0;
}
/**
* Sets the task frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off the task
* &gt; 0 Use the value as a time in minutes
* @param taskFrequency The frequency in time or distance
*/
public void setTaskFrequency(int taskFrequency) {
Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency);
this.taskFrequency = taskFrequency;
restore();
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
}
double getNextTaskDistance() {
return nextTaskDistance;
}
}
@@ -0,0 +1,33 @@
/*
* 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.tasks;
import android.content.Context;
/**
* An interface for classes that can create periodic tasks.
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if task is not supported
*/
PeriodicTask create(Context context);
}
@@ -14,16 +14,22 @@
* the License.
*/
package com.google.android.apps.mytracks.services;
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.Context;
/**
* A simple task to insert statistics markers every n minutes.
* A simple task to insert statistics markers periodically.
* @author Sandor Dornbush
*/
public class TimeSplitTask implements PeriodicTask {
public class SplitTask implements PeriodicTask {
private SplitTask() {
}
@Override
public void run(TrackRecordingService service) {
@@ -37,4 +43,15 @@ public class TimeSplitTask implements PeriodicTask {
@Override
public void start() {
}
/**
* Create new SplitTasks.
*/
public static class Factory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new SplitTask();
}
}
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.util.ApiFeatures;
@@ -26,7 +26,7 @@ import android.media.AudioManager;
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory {
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
private final boolean hasTts;
@@ -34,11 +34,7 @@ public class StatusAnnouncerFactory {
this.hasTts = apiFeatures.hasTextToSpeech();
}
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if announcements are not supported
*/
@Override
public PeriodicTask create(Context context) {
if (hasTts) {
if (ApiFeatures.getInstance().isAudioFocusSupported()) {
@@ -14,11 +14,12 @@
* the License.
*/
package com.google.android.apps.mytracks.services;
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
@@ -101,7 +102,8 @@ public class StatusAnnouncerTask implements PeriodicTask {
*/
protected void onTtsInit(int status) {
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
this.ready = status == TextToSpeech.SUCCESS;
// TTS should be valid here but NPE exceptions were reported to the market.
this.ready = status == TextToSpeech.SUCCESS && tts != null;
if (ready) {
// Force the language to be the same as the string we will be speaking,
@@ -14,9 +14,9 @@
* the License.
*/
package com.google.android.apps.mytracks.services;
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.Constants;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
@@ -24,12 +24,14 @@ import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This class will periodically announce the user's trip statistics.
* This class will periodically perform a task.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecuter {
public class TimerTaskExecutor {
private final PeriodicTask task;
private final TrackRecordingService service;
@@ -40,8 +42,8 @@ public class PeriodicTaskExecuter {
*/
private Timer timer;
public PeriodicTaskExecuter(PeriodicTask task,
TrackRecordingService service) {
public TimerTaskExecutor(PeriodicTask task,
TrackRecordingService service) {
this.task = task;
this.service = service;
}
@@ -77,8 +79,7 @@ public class PeriodicTaskExecuter {
}
Date start = new Date(next);
Log.i(Constants.TAG,
task.getClass().getSimpleName() + " scheduled to start at " + start
Log.i(TAG, task.getClass().getSimpleName() + " scheduled to start at " + start
+ " every " + interval + " milliseconds.");
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
}
@@ -87,8 +88,7 @@ public class PeriodicTaskExecuter {
* Cleans up this object.
*/
public void shutdown() {
Log.i(Constants.TAG,
task.getClass().getSimpleName() + " shutting down.");
Log.i(TAG, task.getClass().getSimpleName() + " shutting down.");
if (timer != null) {
timer.cancel();
timer.purge();