Fixing periodic task restarting.

This commit is contained in:
Rodrigo Damazio
2010-11-21 20:45:25 -08:00
commit e7479c8a93
267 changed files with 39485 additions and 0 deletions
@@ -0,0 +1,119 @@
/*
* 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 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
* current device.
*
* @author Rodrigo Damazio
*/
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);
private static ApiFeatures instance;
/**
* The API platform adapter supported by this system.
*/
private ApiPlatformAdapter apiPlatformAdapter;
/**
* Returns the singleton instance of this class.
*/
public static ApiFeatures getInstance() {
if (instance == null) {
instance = new ApiFeatures();
}
return instance;
}
/**
* Injects a specific singleton instance, to be used for unit tests.
*/
public static void injectInstance(ApiFeatures instance) {
ApiFeatures.instance = instance;
}
/**
* Allow subclasses for mocking, but no direct instantiation.
*/
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.
*/
public boolean hasBackup() {
return getApiLevel() >= 8;
}
/**
* Returns whether text-to-speech is available.
*/
public boolean hasTextToSpeech() {
if (getApiLevel() < 4) return false;
try {
Class.forName("android.speech.tts.TextToSpeech");
} catch (ClassNotFoundException ex) {
return false;
} catch (LinkageError er) {
return false;
}
return true;
}
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,176 @@
/*
* Copyright 2009 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 com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.Vector;
/**
* This class will generate google chart server url's.
*
* @author Sandor Dornbush
*/
public class ChartURLGenerator {
private static final String CHARTS_BASE_URL =
"http://chart.apis.google.com/chart?";
private ChartURLGenerator() {
}
/**
* Gets a chart of a track.
*
* @param distances An array of distance measurements
* @param elevations A matching array of elevation measurements
* @param track The track for this chart
* @param context The current appplication context
*/
public static String getChartUrl(Vector<Double> distances,
Vector<Double> elevations, Track track, Context context) {
SharedPreferences preferences =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
if (preferences != null) {
metricUnits = preferences.getBoolean(
context.getString(R.string.metric_units_key), true);
}
return getChartUrl(distances, elevations, track,
context.getString(R.string.elevation_label), metricUnits, true);
}
/**
* Gets a chart of a track.
* This form is for testing without contexts.
*
* @param distances An array of distance measurements
* @param elevations A matching array of elevation measurements
* @param track The track for this chart
* @param title The title for the chart
* @param metricUnits Should the data be displayed in metric units
* @param escapePipe Should the pipe character be escaped
*/
public static String getChartUrl(
Vector<Double> distances, Vector<Double> elevations,
Track track, String title, boolean metricUnits, boolean escapePipe) {
if (distances == null || elevations == null || track == null) {
return null;
}
if (distances.size() != elevations.size()) {
return null;
}
// Round it up.
TripStatistics stats = track.getStatistics();
double effectiveMaxY = metricUnits
? stats.getMaxElevation()
: stats.getMaxElevation() * UnitConversions.M_TO_FT;
effectiveMaxY = ((int) (effectiveMaxY / 100)) * 100 + 100;
// Round it down.
double effectiveMinY = 0;
double minElevation = metricUnits
? stats.getMinElevation()
: stats.getMinElevation() * UnitConversions.M_TO_FT;
effectiveMinY = ((int) (minElevation / 100)) * 100;
if (stats.getMinElevation() < 0) {
effectiveMinY -= 100;
}
double ySpread = effectiveMaxY - effectiveMinY;
StringBuilder sb = new StringBuilder(CHARTS_BASE_URL);
sb.append("&chs=600x350");
sb.append("&cht=lxy");
// Title
sb.append("&chtt=");
sb.append(title);
// Labels
sb.append("&chxt=x,y");
double distKM = stats.getTotalDistance() / 1000.0;
double distDisplay =
metricUnits ? distKM : (distKM * UnitConversions.KM_TO_MI);
int xInterval = ((int) (distDisplay / 6));
int yInterval = ((int) (ySpread / 600)) * 100;
if (yInterval < 100) {
yInterval = 25;
}
// Range
sb.append("&chxr=0,0,");
sb.append((int) distDisplay);
sb.append(',');
sb.append(xInterval);
// This is an ugly hack. We really want a pipe but we must double escape it.
sb.append(escapePipe ? "%257C" : "|");
sb.append("1,");
sb.append(effectiveMinY);
sb.append(',');
sb.append(effectiveMaxY);
sb.append(',');
sb.append(yInterval);
// Line color
sb.append("&chco=009A00");
// Fill
sb.append("&chm=B,00AA00,0,0,0");
// Grid lines
double desiredGrids = ySpread / yInterval;
sb.append("&chg=100000,");
sb.append(100.0 / desiredGrids);
sb.append(",1,0");
// Data
sb.append("&chd=e:");
for (int i = 0; i < distances.size(); i++) {
int normalized =
(int) (getNormalizedDistance(distances.elementAt(i), track) * 4095);
sb.append(ChartsExtendedEncoder.getEncodedValue(normalized));
}
sb.append(ChartsExtendedEncoder.getSeparator());
for (int i = 0; i < elevations.size(); i++) {
int normalized =
(int) (getNormalizedElevation(
elevations.elementAt(i), effectiveMinY, ySpread) * 4095);
sb.append(ChartsExtendedEncoder.getEncodedValue(normalized));
}
return sb.toString();
}
private static double getNormalizedDistance(double d, Track track) {
return d / track.getStatistics().getTotalDistance();
}
private static double getNormalizedElevation(
double d, double effectiveMinY, double ySpread) {
return (d - effectiveMinY) / ySpread;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2009 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;
/**
* The singleton class representing the extended encoding of chart data.
*/
public class ChartsExtendedEncoder {
// ChartServer data encoding in extended mode
private static final String CHARTSERVER_EXTENDED_ENCODING_SEPARATOR = ",";
private static final String CHARTSERVER_EXTENDED_ENCODING =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
private static final int CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES =
CHARTSERVER_EXTENDED_ENCODING.length();
private static final String MISSING_POINT_EXTENDED_ENCODING = "__";
private ChartsExtendedEncoder() { }
public static String getEncodedValue(int scaled) {
int index1 = scaled / CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
if (index1 < 0 || index1 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
return MISSING_POINT_EXTENDED_ENCODING;
}
int index2 = scaled % CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
if (index2 < 0 || index2 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
return MISSING_POINT_EXTENDED_ENCODING;
}
return String.valueOf(CHARTSERVER_EXTENDED_ENCODING.charAt(index1))
+ String.valueOf(CHARTSERVER_EXTENDED_ENCODING.charAt(index2));
}
public static String getSeparator() {
return CHARTSERVER_EXTENDED_ENCODING_SEPARATOR;
}
}
@@ -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,149 @@
/*
* 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 com.google.android.apps.mytracks.MyTracksConstants;
import android.os.Environment;
import java.io.File;
import java.util.regex.Pattern;
/**
* Utilities for dealing with files.
*
* @author Rodrigo Damazio
*/
public class FileUtils {
/**
* The maximum length of a filename, as per the FAT32 specification.
*/
private static final int MAX_FILENAME_LENGTH = 260;
/**
* A set of characters that are prohibited from being in file names.
*/
private static final Pattern PROHIBITED_CHAR_PATTERN =
Pattern.compile("[^ A-Za-z0-9_.()-]+");
/**
* Builds a path inside the My Tracks directory in the SD card.
*
* @param components the path components inside the mytracks directory
* @return the full path to the destination
*/
public String buildExternalDirectoryPath(String... components) {
StringBuilder dirNameBuilder = new StringBuilder();
dirNameBuilder.append(Environment.getExternalStorageDirectory());
dirNameBuilder.append(File.separatorChar);
dirNameBuilder.append(MyTracksConstants.SDCARD_TOP_DIR);
for (String component : components) {
dirNameBuilder.append(File.separatorChar);
dirNameBuilder.append(component);
}
return dirNameBuilder.toString();
}
/**
* Returns whether the SD card is available.
*/
public boolean isSdCardAvailable() {
return Environment.MEDIA_MOUNTED.equals(
Environment.getExternalStorageState());
}
/**
* Normalizes the input string and make sure it is a valid fat32 file name.
*
* @param name the name to normalize
* @return the sanitized name
*/
String sanitizeName(String name) {
String cleaned = PROHIBITED_CHAR_PATTERN.matcher(name).replaceAll("");
return (cleaned.length() > MAX_FILENAME_LENGTH)
? cleaned.substring(0, MAX_FILENAME_LENGTH)
: cleaned.toString();
}
/**
* Ensures the given directory exists by creating it and its parents if
* necessary.
*
* @return whether the directory exists (either already existed or was
* successfully created)
*/
public boolean ensureDirectoryExists(File dir) {
if (dir.exists() && dir.isDirectory()) {
return true;
}
if (dir.mkdirs()) {
return true;
}
return false;
}
/**
* Builds a filename with the given base name (prefix) and the given
* extension, possibly adding a suffix to ensure the file doesn't exist.
*
* @param directory the directory the file will live in
* @param fileBaseName the prefix for the file name
* @param extension the file's extension
* @return the complete file name, without the directory
*/
public synchronized String buildUniqueFileName(File directory,
String fileBaseName, String extension) {
return buildUniqueFileName(directory, fileBaseName, extension, 0);
}
/**
* Builds a filename with the given base name (prefix) and the given
* extension, possibly adding a suffix to ensure the file doesn't exist.
*
* @param directory the directory the file will live in
* @param fileBaseName the prefix for the file name
* @param extension the file's extension
* @param suffix the first numeric suffix to try to use, or 0 for none
* @return the complete file name, without the directory
*/
private String buildUniqueFileName(File directory, String fileBaseName,
String extension, int suffix) {
String suffixedBaseName = fileBaseName;
if (suffix > 0) {
suffixedBaseName += " (" + Integer.toString(suffix) + ")";
}
String fullName = suffixedBaseName + "." + extension;
String sanitizedName = sanitizeName(fullName);
if (!fileExists(directory, sanitizedName)) {
return sanitizedName;
}
return buildUniqueFileName(directory, fileBaseName, extension, suffix + 1);
}
/**
* Checks whether a file with the given name exists in the given directory.
* This is isolated so it can be overridden in tests.
*/
protected boolean fileExists(File directory, String fullName) {
File file = new File(directory, fullName);
return file.exists();
}
}
@@ -0,0 +1,254 @@
/*
* 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.util;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.GeoPoint;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* Utility class for decimating tracks at a given level of precision.
*
* @author Leif Hendrik Wilden
*/
public class MyTracksUtils {
/**
* Computes the distance on the two sphere between the point c0 and the line
* segment c1 to c2.
*
* @param c0 the first coordinate
* @param c1 the beginning of the line segment
* @param c2 the end of the lone segment
* @return the distance in m (assuming spherical earth)
*/
public static double distance(
final Location c0, final Location c1, final Location c2) {
if (c1.equals(c2)) {
return c2.distanceTo(c0);
}
final double s0lat = c0.getLatitude() * UnitConversions.TO_RADIANS;
final double s0lng = c0.getLongitude() * UnitConversions.TO_RADIANS;
final double s1lat = c1.getLatitude() * UnitConversions.TO_RADIANS;
final double s1lng = c1.getLongitude() * UnitConversions.TO_RADIANS;
final double s2lat = c2.getLatitude() * UnitConversions.TO_RADIANS;
final double s2lng = c2.getLongitude() * UnitConversions.TO_RADIANS;
double s2s1lat = s2lat - s1lat;
double s2s1lng = s2lng - s1lng;
final double u =
((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng)
/ (s2s1lat * s2s1lat + s2s1lng * s2s1lng);
if (u <= 0) {
return c0.distanceTo(c1);
}
if (u >= 1) {
return c0.distanceTo(c2);
}
Location sa = new Location("");
sa.setLatitude(c0.getLatitude() - c1.getLatitude());
sa.setLongitude(c0.getLongitude() - c1.getLongitude());
Location sb = new Location("");
sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude()));
sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude()));
return sa.distanceTo(sb);
}
/**
* Decimates the given locations for a given zoom level. This uses a
* Douglas-Peucker decimation algorithm.
*
* @param tolerance in meters
* @param locations input
* @param decimated output
*/
public static void decimate(double tolerance, ArrayList<Location> locations,
ArrayList<Location> decimated) {
final int n = locations.size();
if (n < 1) {
return;
}
int idx;
int maxIdx = 0;
Stack<int[]> stack = new Stack<int[]>();
double[] dists = new double[n];
dists[0] = 1;
dists[n - 1] = 1;
double maxDist;
double dist = 0.0;
int[] current;
if (n > 2) {
int[] stackVal = new int[] {0, (n - 1)};
stack.push(stackVal);
while (stack.size() > 0) {
current = stack.pop();
maxDist = 0;
for (idx = current[0] + 1; idx < current[1]; ++idx) {
dist = MyTracksUtils.distance(
locations.get(idx),
locations.get(current[0]),
locations.get(current[1]));
if (dist > maxDist) {
maxDist = dist;
maxIdx = idx;
}
}
if (maxDist > tolerance) {
dists[maxIdx] = maxDist;
int[] stackValCurMax = {current[0], maxIdx};
stack.push(stackValCurMax);
int[] stackValMaxCur = {maxIdx, current[1]};
stack.push(stackValMaxCur);
}
}
}
int i = 0;
idx = 0;
decimated.clear();
for (Location l : locations) {
if (dists[idx] != 0) {
decimated.add(l);
i++;
}
idx++;
}
Log.d(MyTracksConstants.TAG, "Decimating " + n + " points to " + i
+ " w/ tolerance = " + tolerance);
}
/**
* Decimates the given track for the given precision.
*
* @param track a track
* @param precision desired precision in meters
*/
public static void decimate(Track track, double precision) {
ArrayList<Location> decimated = new ArrayList<Location>();
decimate(precision, track.getLocations(), decimated);
track.setLocations(decimated);
}
/**
* Limits number of points by dropping any points beyond the given number of
* points. Note: That'll actually discard points.
*
* @param track a track
* @param numberOfPoints maximum number of points
*/
public static void cut(Track track, int numberOfPoints) {
ArrayList<Location> locations = track.getLocations();
while (locations.size() > numberOfPoints) {
locations.remove(locations.size() - 1);
}
}
/**
* Splits a track in multiple tracks where each piece has less or equal than
* maxPoints.
*
* @param track the track to split
* @param maxPoints maximum number of points for each piece
* @return a list of one or more track pieces
*/
public static ArrayList<Track> split(Track track, int maxPoints) {
ArrayList<Track> result = new ArrayList<Track>();
final int nTotal = track.getLocations().size();
int n = 0;
Track piece = null;
do {
piece = new Track();
TripStatistics pieceStats = piece.getStatistics();
piece.setId(track.getId());
piece.setName(track.getName());
piece.setDescription(track.getDescription());
piece.setCategory(track.getCategory());
List<Location> pieceLocations = piece.getLocations();
for (int i = n; i < nTotal && pieceLocations.size() < maxPoints; i++) {
piece.addLocation(track.getLocations().get(i));
}
int nPointsPiece = pieceLocations.size();
if (nPointsPiece >= 2) {
pieceStats.setStartTime(pieceLocations.get(0).getTime());
pieceStats.setStopTime(pieceLocations.get(nPointsPiece - 1).getTime());
result.add(piece);
}
n += (pieceLocations.size() - 1);
} while (n < nTotal && piece != null && piece.getLocations().size() > 1);
return result;
}
/**
* Test if a given GeoPoint is valid, i.e. within physical bounds.
*
* @param geoPoint the point to be tested
* @return true, if it is a physical location on earth.
*/
public static boolean isValidGeoPoint(GeoPoint geoPoint) {
return Math.abs(geoPoint.getLatitudeE6()) < 90E6
&& Math.abs(geoPoint.getLongitudeE6()) <= 180E6;
}
/**
* Checks if a given location is a valid (i.e. physically possible) location
* on Earth. Note: The special separator locations (which have latitude =
* 100) will not qualify as valid. Neither will locations with lat=0 and lng=0
* as these are most likely "bad" measurements which often cause trouble.
*
* @param location the location to test
* @return true if the location is a valid location.
*/
public static boolean isValidLocation(Location location) {
return location != null && Math.abs(location.getLatitude()) <= 90
&& Math.abs(location.getLongitude()) <= 180;
}
/**
* Gets a location from a GeoPoint.
*
* @param p a GeoPoint
* @return the corresponding location
*/
public static Location getLocation(GeoPoint p) {
Location result = new Location("");
result.setLatitude(p.getLatitudeE6() / 1.0E6);
result.setLongitude(p.getLongitudeE6() / 1.0E6);
return result;
}
public static GeoPoint getGeoPoint(Location location) {
return new GeoPoint((int) (location.getLatitude() * 1E6),
(int) (location.getLongitude() * 1E6));
}
/**
* This is a utility class w/ only static memebers.
*/
protected MyTracksUtils() {
}
}
@@ -0,0 +1,84 @@
/*
* 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.util;
import android.content.Context;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
/**
* Utility functions for android resources.
*
* @author Sandor Dornbush
*/
public class ResourceUtils {
public static CharSequence readFile(Context activity, int id) {
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(
activity.getResources().openRawResource(id)));
String line;
StringBuilder buffer = new StringBuilder();
while ((line = in.readLine()) != null) {
buffer.append(line).append('\n');
}
return buffer;
} catch (IOException e) {
return "";
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// Ignore
}
}
}
}
public static void readBinaryFileToOutputStream(
Context activity, int id, OutputStream os) {
BufferedInputStream in = null;
try {
in = new BufferedInputStream(
activity.getResources().openRawResource(id));
BufferedOutputStream out = new BufferedOutputStream(os);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
out.flush();
} catch (IOException e) {
return;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// Ignore
}
}
}
}
private ResourceUtils() {
}
}
@@ -0,0 +1,480 @@
/*
* 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.util;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
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.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Various string manipulation methods.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class StringUtils implements DescriptionGenerator {
private final Context context;
/**
* Formats a number of milliseconds as a string.
*
* @param time - A period of time in milliseconds.
* @return A string of the format M:SS, MM:SS or HH:MM:SS
*/
public static String formatTime(long time) {
return formatTimeInternal(time, false);
}
/**
* Formats a number of milliseconds as a string. To be used when we need the
* hours to be shown even when it is zero, e.g. exporting data to a
* spreadsheet.
*
* @param time - A period of time in milliseconds
* @return A string of the format HH:MM:SS even if time is less than 1 hour
*/
public static String formatTimeAlwaysShowingHours(long time) {
return formatTimeInternal(time, true);
}
/**
* Formats the given text as a CDATA element to be used in a XML file. This
* includes adding the starting and ending CDATA tags. Please notice that this
* may result in multiple consecutive CDATA tags.
*
* @param unescaped the unescaped text to be formatted
* @return the formatted text, inside one or more CDATA tags
*/
public static String stringAsCData(String unescaped) {
// "]]>" needs to be broken into multiple CDATA segments, like:
// "Foo]]>Bar" becomes "<![CDATA[Foo]]]]><![CDATA[>Bar]]>"
// (the end of the first CDATA has the "]]", the other has ">")
String escaped = unescaped.replaceAll("]]>", "]]]]><![CDATA[>");
return "<![CDATA[" + escaped + "]]>";
}
private static final SimpleDateFormat BASE_XML_DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
static {
BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC"));
}
private static final Pattern XML_DATE_EXTRAS_PATTERN =
Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
/**
* Parses an XML dateTime element as defined by the XML standard.
*
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">dateTime</a>
*/
public static long parseXmlDateTime(String xmlTime) {
// Parse the base date (fixed format)
ParsePosition position = new ParsePosition(0);
Date date = BASE_XML_DATE_FORMAT.parse(xmlTime, position);
if (date == null) {
throw new IllegalArgumentException("Invalid XML dateTime value: '" + xmlTime
+ "' (at position " + position.getErrorIndex() + ")");
}
// Parse the extras
Matcher matcher =
XML_DATE_EXTRAS_PATTERN.matcher(xmlTime.substring(position.getIndex()));
if (!matcher.matches()) {
// This will match even an empty string as all groups are optional,
// so a non-match means some other garbage was there
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlTime);
}
long time = date.getTime();
// Account for fractional seconds
String fractional = matcher.group(1);
if (fractional != null) {
// Regex ensures fractional part is in (0,1(
float fractionalSeconds = Float.parseFloat(fractional);
long fractionalMillis = (long) (fractionalSeconds * 1000.0f);
time += fractionalMillis;
}
// Account for timezones
String sign = matcher.group(2);
String offsetHoursStr = matcher.group(3);
String offsetMinsStr = matcher.group(4);
if (sign != null && offsetHoursStr != null && offsetMinsStr != null) {
// Regex ensures sign is + or -
boolean plusSign = sign.equals("+");
int offsetHours = Integer.parseInt(offsetHoursStr);
int offsetMins = Integer.parseInt(offsetMinsStr);
// Regex ensures values are >= 0
if (offsetHours > 14 || offsetMins > 59) {
throw new IllegalArgumentException("Bad timezone in " + xmlTime);
}
long totalOffsetMillis = (offsetMins + offsetHours * 60L) * 60000L;
// Make time go back to UTC
if (plusSign) {
time -= totalOffsetMillis;
} else {
time += totalOffsetMillis;
}
}
return time;
}
/**
* Formats a number of milliseconds as a string.
*
* @param time - A period of time in milliseconds
* @param alwaysShowHours - Whether to display 00 hours if time is less than 1
* hour
* @return A string of the format HH:MM:SS
*/
private static String formatTimeInternal(long time, boolean alwaysShowHours) {
int[] parts = getTimeParts(time);
StringBuilder builder = new StringBuilder();
if (parts[2] > 0 || alwaysShowHours) {
builder.append(parts[2]);
builder.append(':');
if (parts[1] <= 9) {
builder.append("0");
}
}
builder.append(parts[1]);
builder.append(':');
if (parts[0] <= 9) {
builder.append("0");
}
builder.append(parts[0]);
return builder.toString();
}
/**
* Gets the time as an array of parts.
*/
public static int[] getTimeParts(long time) {
if (time < 0) {
int[] parts = getTimeParts(time * -1);
parts[0] *= -1;
parts[1] *= -1;
parts[2] *= -1;
return parts;
}
int[] parts = new int[3];
long seconds = time / 1000;
parts[0] = (int) (seconds % 60);
int tmp = (int) (seconds / 60);
parts[1] = tmp % 60;
parts[2] = tmp / 60;
return parts;
}
public StringUtils(Context context) {
this.context = context;
}
public String formatTimeLong(long time) {
int[] parts = getTimeParts(time);
String secLabel =
context.getString(parts[0] == 1 ? R.string.second : R.string.seconds);
String minLabel =
context.getString(parts[1] == 1 ? R.string.minute : R.string.minutes);
String hourLabel =
context.getString(parts[2] == 1 ? R.string.hour : R.string.hours);
StringBuilder sb = new StringBuilder();
if (parts[2] != 0) {
sb.append(parts[2]);
sb.append(" ");
sb.append(hourLabel);
sb.append(" ");
sb.append(parts[1]);
sb.append(minLabel);
} else {
sb.append(parts[1]);
sb.append(" ");
sb.append(minLabel);
sb.append(" ");
sb.append(parts[0]);
sb.append(secLabel);
}
return sb.toString();
}
/**
* Generates a description for a track (with information about the
* statistics).
*
* @param track the track
* @return a track description
*/
public String generateTrackDescription(Track track, Vector<Double> distances,
Vector<Double> elevations) {
boolean displaySpeed = true;
SharedPreferences preferences =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (preferences != null) {
displaySpeed =
preferences.getBoolean(context.getString(R.string.report_speed_key), true);
}
TripStatistics trackStats = track.getStatistics();
final double distanceInKm = trackStats.getTotalDistance() / 1000;
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
final long minElevationInMeters = Math.round(trackStats.getMinElevation());
final long minElevationInFeet =
Math.round(trackStats.getMinElevation() * UnitConversions.M_TO_FT);
final long maxElevationInMeters = Math.round(trackStats.getMaxElevation());
final long maxElevationInFeet =
Math.round(trackStats.getMaxElevation() * UnitConversions.M_TO_FT);
final long elevationGainInMeters =
Math.round(trackStats.getTotalElevationGain());
final long elevationGainInFeet = Math.round(
trackStats.getTotalElevationGain() * UnitConversions.M_TO_FT);
long minGrade = 0;
long maxGrade = 0;
double trackMaxGrade = trackStats.getMaxGrade();
double trackMinGrade = trackStats.getMinGrade();
if (!Double.isNaN(trackMaxGrade)
&& !Double.isInfinite(trackMaxGrade)) {
maxGrade = Math.round(trackMaxGrade * 100);
}
if (!Double.isNaN(trackMinGrade) && !Double.isInfinite(trackMinGrade)) {
minGrade = Math.round(trackMinGrade * 100);
}
String category = context.getString(R.string.unknown);
String trackCategory = track.getCategory();
if (trackCategory != null && trackCategory.length() > 0) {
category = trackCategory;
}
String averageSpeed =
getSpeedString(trackStats.getAverageSpeed(),
R.string.average_speed_label,
R.string.average_pace_label,
displaySpeed);
String averageMovingSpeed =
getSpeedString(trackStats.getAverageMovingSpeed(),
R.string.average_moving_speed_label,
R.string.average_moving_pace_label,
displaySpeed);
String maxSpeed =
getSpeedString(trackStats.getMaxSpeed(),
R.string.max_speed_label,
R.string.min_pace_label,
displaySpeed);
return String.format("%s<p>"
+ "%s: %.2f %s (%.1f %s)<br>"
+ "%s: %s<br>"
+ "%s: %s<br>"
+ "%s %s %s"
+ "%s: %d %s (%d %s)<br>"
+ "%s: %d %s (%d %s)<br>"
+ "%s: %d %s (%d %s)<br>"
+ "%s: %d %%<br>"
+ "%s: %d %%<br>"
+ "%s: %tc<br>"
+ "%s: %s<br>"
+ "<img border=\"0\" src=\"%s\"/>",
// Line 1
context.getString(R.string.my_map_link),
// Line 2
context.getString(R.string.total_distance_label),
distanceInKm, context.getString(R.string.kilometer),
distanceInMiles, context.getString(R.string.mile),
// Line 3
context.getString(R.string.total_time_label),
StringUtils.formatTime(trackStats.getTotalTime()),
// Line 4
context.getString(R.string.moving_time_label),
StringUtils.formatTime(trackStats.getMovingTime()),
// Line 5
averageSpeed, averageMovingSpeed, maxSpeed,
// Line 6
context.getString(R.string.min_elevation_label),
minElevationInMeters, context.getString(R.string.meter),
minElevationInFeet, context.getString(R.string.feet),
// Line 7
context.getString(R.string.max_elevation_label),
maxElevationInMeters, context.getString(R.string.meter),
maxElevationInFeet, context.getString(R.string.feet),
// Line 8
context.getString(R.string.elevation_gain_label),
elevationGainInMeters, context.getString(R.string.meter),
elevationGainInFeet, context.getString(R.string.feet),
// Line 9
context.getString(R.string.max_grade_label), maxGrade,
// Line 10
context.getString(R.string.min_grade_label), minGrade,
// Line 11
context.getString(R.string.recorded_date),
new Date(trackStats.getStartTime()),
// Line 12
context.getString(R.string.category), category,
// Line 13
ChartURLGenerator.getChartUrl(distances, elevations, track, context));
}
private String getSpeedString(double speed, int speedLabel, int paceLabel,
boolean displaySpeed) {
double speedInKph = speed * 3.6;
double speedInMph = speedInKph * UnitConversions.KMH_TO_MPH;
if (displaySpeed) {
return String.format("%s: %.2f %s (%.1f %s)<br>",
context.getString(speedLabel),
speedInKph, context.getString(R.string.kilometer_per_hour),
speedInMph, context.getString(R.string.mile_per_hour));
} else {
double paceInKm;
double paceInMi;
if (speed == 0) {
paceInKm = 0.0;
paceInMi = 0.0;
} else {
paceInKm = 60.0 / speedInKph;
paceInMi = 60.0 / speedInMph;
}
return String.format("%s: %.2f %s (%.1f %s)<br>",
context.getString(paceLabel),
paceInKm, context.getString(R.string.min_per_kilometer),
paceInMi, context.getString(R.string.min_per_mile));
}
}
/**
* Generates a description for a waypoint (with information about the
* statistics).
*
* @return a track description
*/
public String generateWaypointDescription(Waypoint waypoint) {
TripStatistics stats = waypoint.getStatistics();
final double distanceInKm = stats.getTotalDistance() / 1000;
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
final double averageSpeedInKmh = stats.getAverageSpeed() * 3.6;
final double averageSpeedInMph =
averageSpeedInKmh * UnitConversions.KMH_TO_MPH;
final double movingSpeedInKmh = stats.getAverageMovingSpeed() * 3.6;
final double movingSpeedInMph =
movingSpeedInKmh * UnitConversions.KMH_TO_MPH;
final double maxSpeedInKmh = stats.getMaxSpeed() * 3.6;
final double maxSpeedInMph = maxSpeedInKmh * UnitConversions.KMH_TO_MPH;
final long minElevationInMeters = Math.round(stats.getMinElevation());
final long minElevationInFeet =
Math.round(stats.getMinElevation() * UnitConversions.M_TO_FT);
final long maxElevationInMeters = Math.round(stats.getMaxElevation());
final long maxElevationInFeet =
Math.round(stats.getMaxElevation() * UnitConversions.M_TO_FT);
final long elevationGainInMeters =
Math.round(stats.getTotalElevationGain());
final long elevationGainInFeet = Math.round(
stats.getTotalElevationGain() * UnitConversions.M_TO_FT);
long theMinGrade = 0;
long theMaxGrade = 0;
double maxGrade = stats.getMaxGrade();
double minGrade = stats.getMinGrade();
if (!Double.isNaN(maxGrade) &&
!Double.isInfinite(maxGrade)) {
theMaxGrade = Math.round(maxGrade * 100);
}
if (!Double.isNaN(minGrade) &&
!Double.isInfinite(minGrade)) {
theMinGrade = Math.round(minGrade * 100);
}
final String percent = "%";
return String.format(
"%s: %.2f %s (%.1f %s)\n"
+ "%s: %s\n"
+ "%s: %s\n"
+ "%s: %.2f %s (%.1f %s)\n"
+ "%s: %.2f %s (%.1f %s)\n"
+ "%s: %.2f %s (%.1f %s)\n"
+ "%s: %d %s (%d %s)\n"
+ "%s: %d %s (%d %s)\n"
+ "%s: %d %s (%d %s)\n"
+ "%s: %d %s\n"
+ "%s: %d %s\n",
context.getString(R.string.distance_label),
distanceInKm, context.getString(R.string.kilometer),
distanceInMiles, context.getString(R.string.mile),
context.getString(R.string.time_label),
StringUtils.formatTime(stats.getTotalTime()),
context.getString(R.string.moving_time_label),
StringUtils.formatTime(stats.getMovingTime()),
context.getString(R.string.average_speed_label),
averageSpeedInKmh, context.getString(R.string.kilometer_per_hour),
averageSpeedInMph, context.getString(R.string.mile_per_hour),
context.getString(R.string.average_moving_speed_label),
movingSpeedInKmh, context.getString(R.string.kilometer_per_hour),
movingSpeedInMph, context.getString(R.string.mile_per_hour),
context.getString(R.string.max_speed_label),
maxSpeedInKmh, context.getString(R.string.kilometer_per_hour),
maxSpeedInMph, context.getString(R.string.mile_per_hour),
context.getString(R.string.min_elevation_label),
minElevationInMeters, context.getString(R.string.meter),
minElevationInFeet, context.getString(R.string.feet),
context.getString(R.string.max_elevation_label),
maxElevationInMeters, context.getString(R.string.meter),
maxElevationInFeet, context.getString(R.string.feet),
context.getString(R.string.elevation_gain_label),
elevationGainInMeters, context.getString(R.string.meter),
elevationGainInFeet, context.getString(R.string.feet),
context.getString(R.string.max_grade_label),
theMaxGrade, percent,
context.getString(R.string.min_grade_label),
theMinGrade, percent);
}
}
@@ -0,0 +1,34 @@
/*
* 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.util;
/**
* Unit conversion constants.
*
* @author Sandor Dornbush
*/
public abstract class UnitConversions {
public static final double KM_TO_MI = 0.621371192;
public static final double M_TO_FT = 3.2808399;
public static final double MI_TO_M = 1609.344;
public static final double MI_TO_FEET = 5280.0;
public static final double KMH_TO_MPH = 1000 * M_TO_FT / MI_TO_FEET;
public static final double TO_RADIANS = Math.PI / 180.0;
protected UnitConversions() {
}
}