Merging from head.

This commit is contained in:
Sandor Dornbush
2010-10-26 14:44:31 -07:00
31 changed files with 1740 additions and 1325 deletions
-1
View File
@@ -38,7 +38,6 @@ limitations under the License.
<string name="share_url_only_key">shareUrlOnly</string> <string name="share_url_only_key">shareUrlOnly</string>
<string name="signal_sampling_frequency_key">signalSamplingFrequency</string> <string name="signal_sampling_frequency_key">signalSamplingFrequency</string>
<string name="split_frequency_key">splitFrequency</string> <string name="split_frequency_key">splitFrequency</string>
<string name="debug_draw_bounds_key">debugDrawBounds</string>
<!-- Preference keys which keep transient state (not user preferences) - DO NOT TRANSLATE --> <!-- Preference keys which keep transient state (not user preferences) - DO NOT TRANSLATE -->
<string name="auto_resume_track_current_retry_key">autoResumeTrackCurrentRetry</string> <string name="auto_resume_track_current_retry_key">autoResumeTrackCurrentRetry</string>
-9
View File
@@ -133,13 +133,4 @@ limitations under the License.
android:persistent="false" /> android:persistent="false" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>
<!-- Debug settings - uncomment this while debugging only -->
<!--
<PreferenceScreen android:title="Debug">
<CheckBoxPreference android:key="@string/debug_draw_bounds_key"
android:title="Draw bounding box"
android:defaultValue="false" />
</PreferenceScreen>
-->
</PreferenceScreen> </PreferenceScreen>
@@ -21,8 +21,10 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackPointsColumns; import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -35,7 +37,6 @@ import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@@ -243,7 +244,9 @@ public class ChartActivity extends Activity implements
providerUtils = MyTracksProviderUtils.Factory.get(this); providerUtils = MyTracksProviderUtils.Factory.get(this);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_elevation); setContentView(R.layout.mytracks_elevation);
@@ -26,11 +26,14 @@ import com.google.android.apps.mytracks.io.AuthManagerFactory;
import com.google.android.apps.mytracks.io.GpxImporter; import com.google.android.apps.mytracks.io.GpxImporter;
import com.google.android.apps.mytracks.io.SendToDocs; import com.google.android.apps.mytracks.io.SendToDocs;
import com.google.android.apps.mytracks.io.SendToMyMaps; import com.google.android.apps.mytracks.io.SendToMyMaps;
import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.TrackWriter; import com.google.android.apps.mytracks.io.TrackWriter;
import com.google.android.apps.mytracks.io.TrackWriterFactory; import com.google.android.apps.mytracks.io.TrackWriterFactory;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.services.ITrackRecordingService; 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.TrackRecordingService;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -53,17 +56,18 @@ import android.location.LocationManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.View.OnTouchListener; import android.view.View.OnTouchListener;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager.BadTokenException; import android.view.WindowManager.BadTokenException;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TabHost; import android.widget.TabHost;
@@ -258,7 +262,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
menuManager = new MenuManager(this); menuManager = new MenuManager(this);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -520,8 +526,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
if (results.hasExtra("mapid")) { if (results.hasExtra("mapid")) {
sendToMyMapsMapId = results.getStringExtra("mapid"); sendToMyMapsMapId = results.getStringExtra("mapid");
} }
setProgressMessage(getString( setProgressMessage(
R.string.progress_message_authenticating_mymaps)); R.string.progress_message_authenticating_mymaps);
authenticate(results, MyTracksConstants.SEND_TO_GOOGLE, authenticate(results, MyTracksConstants.SEND_TO_GOOGLE,
MyMapsConstants.MAPSHOP_SERVICE); MyMapsConstants.MAPSHOP_SERVICE);
} else { } else {
@@ -587,7 +593,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
setProgressValue(0); setProgressValue(0);
setProgressMessage( setProgressMessage(
getString(R.string.progress_message_authenticating_docs)); R.string.progress_message_authenticating_docs);
authenticate(results, authenticate(results,
MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely"); MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely");
} else { } else {
@@ -599,7 +605,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
setProgressValue(30); setProgressValue(30);
setProgressMessage( setProgressMessage(
getString(R.string.progress_message_authenticating_docs)); R.string.progress_message_authenticating_docs);
authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise"); authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise");
} else { } else {
dismissDialogSafely(DIALOG_PROGRESS); dismissDialogSafely(DIALOG_PROGRESS);
@@ -610,7 +616,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
if (results != null && resultCode == RESULT_OK) { if (results != null && resultCode == RESULT_OK) {
Log.d(MyTracksConstants.TAG, "Sending to Docs...."); Log.d(MyTracksConstants.TAG, "Sending to Docs....");
setProgressValue(50); setProgressValue(50);
setProgressMessage(getString(R.string.progress_message_sending_docs)); setProgressMessage(R.string.progress_message_sending_docs);
final long trackId = results.getLongExtra("trackid", selectedTrackId); final long trackId = results.getLongExtra("trackid", selectedTrackId);
final SendToDocs sender = new SendToDocs(this, authMap.get("wise"), final SendToDocs sender = new SendToDocs(this, authMap.get("wise"),
authMap.get("writely"), trackId); authMap.get("writely"), trackId);
@@ -641,30 +647,30 @@ public class MyTracks extends TabActivity implements OnTouchListener,
} }
case MyTracksConstants.SEND_TO_GOOGLE: { case MyTracksConstants.SEND_TO_GOOGLE: {
if (results != null && resultCode == RESULT_OK) { if (results != null && resultCode == RESULT_OK) {
final String mapid; final String mapId;
final long trackId; final long trackId;
if (results.hasExtra("mapid")) { if (results.hasExtra("mapid")) {
mapid = results.getStringExtra("mapid"); mapId = results.getStringExtra("mapid");
} else { } else {
mapid = "new"; mapId = "new";
} }
if (results.hasExtra("trackid")) { if (results.hasExtra("trackid")) {
trackId = results.getLongExtra("trackid", -1); trackId = results.getLongExtra("trackid", -1);
} else { } else {
trackId = selectedTrackId; trackId = selectedTrackId;
} }
final SendToMyMaps sender = new SendToMyMaps(this, mapid, auth,
trackId, this/*progressIndicator*/); OnSendCompletedListener onCompletion = new OnSendCompletedListener() {
Runnable onCompletion = new Runnable() { @Override
public void run() { public void onSendCompleted(String mapId, boolean success, int statusMessage) {
sendToMyMapsMessage = sender.getStatusMessage(); sendToMyMapsMessage = getString(statusMessage);
sendToMyMapsSuccess = sender.wasSuccess(); sendToMyMapsSuccess = success;
if (sendToMyMapsSuccess) { if (sendToMyMapsSuccess) {
sendToMyMapsMapId = sender.getMapId(); sendToMyMapsMapId = mapId;
// Update the map id for this track: // Update the map id for this track:
try { try {
Track track = providerUtils.getTrack(trackId); Track track = providerUtils.getTrack(trackId);
track.setMapId(sender.getMapId()); track.setMapId(mapId);
providerUtils.updateTrack(track); providerUtils.updateTrack(track);
} catch (RuntimeException e) { } catch (RuntimeException e) {
// If that fails whatever reasons we'll just log an error, but // If that fails whatever reasons we'll just log an error, but
@@ -685,8 +691,13 @@ public class MyTracks extends TabActivity implements OnTouchListener,
} }
} }
}; };
sender.setOnCompletion(onCompletion); final SendToMyMaps sender = new SendToMyMaps(this, mapId, auth,
sender.run(); trackId, this/*progressIndicator*/, onCompletion);
HandlerThread handlerThread = new HandlerThread("SendToMyMaps");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(sender);
} else { } else {
dismissDialogSafely(DIALOG_PROGRESS); dismissDialogSafely(DIALOG_PROGRESS);
} }
@@ -950,18 +961,31 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}); });
} }
public void setProgressMessage(final String text) { // ProgressIndicator implementation
@Override
public void setProgressMessage(int resId) {
setProgressMessage(getString(resId));
}
private void setProgressMessage(final String message) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
synchronized (this) { synchronized (this) {
if (progressDialog != null) { if (progressDialog != null) {
progressDialog.setMessage(text); progressDialog.setMessage(message);
} }
} }
} }
}); });
} }
@Override
public void clearProgressMessage() {
setProgressMessage("");
}
@Override
public void setProgressValue(final int percent) { public void setProgressValue(final int percent) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
@@ -1275,7 +1299,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
return; return;
} }
setProgressValue(0); setProgressValue(0);
setProgressMessage(""); clearProgressMessage();
showDialogSafely(DIALOG_PROGRESS); showDialogSafely(DIALOG_PROGRESS);
if (sendToGoogleDialog.getSendToMyMaps()) { if (sendToGoogleDialog.getSendToMyMaps()) {
if (!sendToGoogleDialog.getCreateNewMap()) { if (!sendToGoogleDialog.getCreateNewMap()) {
@@ -1283,8 +1307,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
startActivityForResult(listIntent, MyTracksConstants.GET_MAP); startActivityForResult(listIntent, MyTracksConstants.GET_MAP);
} else { } else {
setProgressValue(0); setProgressValue(0);
setProgressMessage(getString( setProgressMessage(
R.string.progress_message_authenticating_mymaps)); R.string.progress_message_authenticating_mymaps);
authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE, authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE,
MyMapsConstants.MAPSHOP_SERVICE); MyMapsConstants.MAPSHOP_SERVICE);
} }
@@ -15,8 +15,6 @@
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import android.os.Build;
/** /**
* Constants used by the MyTracks application. * Constants used by the MyTracks application.
* *
@@ -40,11 +38,6 @@ public abstract class MyTracksConstants {
*/ */
public static final String SDCARD_TOP_DIR = "MyTracks"; public static final String SDCARD_TOP_DIR = "MyTracks";
/**
* The API level of the Android version we're being run under.
*/
public static final int ANDROID_API_LEVEL = Integer.parseInt(Build.VERSION.SDK);
/* /*
* onActivityResult request codes: * onActivityResult request codes:
*/ */
@@ -21,7 +21,9 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackPointsColumns; import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity; import com.google.android.maps.MapActivity;
@@ -47,17 +49,16 @@ import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.provider.Settings; import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.SubMenu; import android.view.SubMenu;
import android.view.View; import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.view.Window; import android.view.Window;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
@@ -139,14 +140,16 @@ public class MyTracksMap extends MapActivity
*/ */
private HandlerThread updateTrackThread; private HandlerThread updateTrackThread;
/** Handler for updateTrackThread */ /**
* Handler for updateTrackThread.
*/
private Handler updateTrackHandler; private Handler updateTrackHandler;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
/** /**
* A runnable that updates the track from the provider (looking for points * A runnable that updates the track from the provider (looking for points
* added after "lastSeenLocationId". * added after "lastSeenLocationId").
*/ */
private final Runnable updateTrackRunnable = new Runnable() { private final Runnable updateTrackRunnable = new Runnable() {
@Override @Override
@@ -154,6 +157,7 @@ public class MyTracksMap extends MapActivity
if (!isATrackSelected()) { if (!isATrackSelected()) {
return; return;
} }
readAllNewTrackPoints(); readAllNewTrackPoints();
} }
}; };
@@ -167,6 +171,7 @@ public class MyTracksMap extends MapActivity
if (!isATrackSelected()) { if (!isATrackSelected()) {
return; return;
} }
mapOverlay.clearPoints(); mapOverlay.clearPoints();
firstSeenLocationId = -1; firstSeenLocationId = -1;
lastSeenLocationId = -1; lastSeenLocationId = -1;
@@ -192,13 +197,10 @@ public class MyTracksMap extends MapActivity
cursor = providerUtils.getWaypointsCursor( cursor = providerUtils.getWaypointsCursor(
selectedTrackId, 0, selectedTrackId, 0,
MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS); MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS);
if (cursor != null) { if (cursor != null && cursor.moveToFirst()) {
if (cursor.moveToFirst()) { do {
do { mapOverlay.addWaypoint(providerUtils.createWaypoint(cursor));
Waypoint wpt = providerUtils.createWaypoint(cursor); } while (cursor.moveToNext());
mapOverlay.addWaypoint(wpt);
} while (cursor.moveToNext());
}
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e); Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
@@ -212,9 +214,9 @@ public class MyTracksMap extends MapActivity
}; };
/** /**
* A runnable intended to be posted to the updateTrackThread after the * A runnable intended to be posted to the {@code #updateTrackThread} after
* selected track changes. It will post to the ui thread to update the screen * the selected track changes. It will post to the UI thread to update
* elements and move the map to show the selected track. * the screen elements and move the map to show the selected track.
*/ */
private final Runnable setSelectedTrackRunnable = new Runnable() { private final Runnable setSelectedTrackRunnable = new Runnable() {
@Override @Override
@@ -282,7 +284,9 @@ public class MyTracksMap extends MapActivity
super.onCreate(bundle); super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
providerUtils = MyTracksProviderUtils.Factory.get(this); providerUtils = MyTracksProviderUtils.Factory.get(this);
@@ -372,12 +376,20 @@ public class MyTracksMap extends MapActivity
setSelectedTrack(selectedTrackId); setSelectedTrack(selectedTrackId);
} }
updateOptionsButton(); updateOptionsButton();
mapOverlay.setDrawBounds(preferences.getBoolean(
getString(R.string.debug_draw_bounds_key), false));
preferences.registerOnSharedPreferenceChangeListener(this); preferences.registerOnSharedPreferenceChangeListener(this);
} }
} }
@Override
protected void onDestroy() {
Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy");
if (updateTrackThread != null) {
updateTrackThread.quit();
}
super.onDestroy();
}
/** /**
* Returns whether there's a track currently selected for display. * Returns whether there's a track currently selected for display.
*/ */
@@ -893,8 +905,7 @@ public class MyTracksMap extends MapActivity
@Override @Override
public void onSharedPreferenceChanged( public void onSharedPreferenceChanged(
final SharedPreferences sharedPreferences, final SharedPreferences sharedPreferences, final String key) {
final String key) {
if (key != null) { if (key != null) {
uiHandler.post(new Runnable() { uiHandler.post(new Runnable() {
@Override @Override
@@ -918,10 +929,6 @@ public class MyTracksMap extends MapActivity
mapOverlay.setShowEndMarker(!isRecordingSelected()); mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.postInvalidate(); mapView.postInvalidate();
} }
} else if (key.equals(getString(R.string.debug_draw_bounds_key))) {
mapOverlay.setDrawBounds(
sharedPreferences.getBoolean(
getString(R.string.debug_draw_bounds_key), false));
} }
} }
}); });
@@ -950,8 +957,8 @@ public class MyTracksMap extends MapActivity
public void onLocationChanged(Location location) { public void onLocationChanged(Location location) {
if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) { if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) {
// Recalculate the variation if there was a jump in location > 1km: // Recalculate the variation if there was a jump in location > 1km:
if (currentLocation == null if (currentLocation == null ||
|| location.distanceTo(currentLocation) > 1000) { location.distanceTo(currentLocation) > 1000) {
setVariation(location); setVariation(location);
} }
currentLocation = location; currentLocation = location;
@@ -1039,8 +1046,8 @@ public class MyTracksMap extends MapActivity
// Now we already have at least one point, calculate the sampling // Now we already have at least one point, calculate the sampling
// frequency // frequency
long numTotalPoints = lastStoredLocationId - firstSeenLocationId; long numTotalPoints = lastStoredLocationId - firstSeenLocationId;
samplingFrequency = (int) (1 + samplingFrequency = (int) (1 + numTotalPoints
numTotalPoints / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS);
} }
providerUtils.fillLocation(cursor, location); providerUtils.fillLocation(cursor, location);
@@ -1052,8 +1059,8 @@ public class MyTracksMap extends MapActivity
if (numPoints % samplingFrequency == 0 || if (numPoints % samplingFrequency == 0 ||
(!isRecordingSelected() && locationId == lastStoredLocationId) || (!isRecordingSelected() && locationId == lastStoredLocationId) ||
!MyTracksUtils.isValidLocation(location)) { !MyTracksUtils.isValidLocation(location)) {
// Only allocate a new location if it is going to be kept around. // No need to allocate a new location (we can safely reuse the existing).
mapOverlay.addLocation(new Location(location)); mapOverlay.addLocation(location);
} }
numPoints++; numPoints++;
@@ -20,20 +20,24 @@ import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView; import com.google.android.maps.MapView;
import com.google.android.maps.Overlay; import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.location.Location; import android.location.Location;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/** /**
* A map overlay that displays a "MyLocation" arrow, an error circle, the * A map overlay that displays a "MyLocation" arrow, an error circle, the
@@ -43,7 +47,7 @@ import java.util.ArrayList;
*/ */
public class MyTracksOverlay extends Overlay { public class MyTracksOverlay extends Overlay {
private final Drawable arrow[] = new Drawable[18]; private final Drawable[] arrows;
private final int arrowWidth, arrowHeight; private final int arrowWidth, arrowHeight;
private final Drawable statsMarker; private final Drawable statsMarker;
private final Drawable waypointMarker; private final Drawable waypointMarker;
@@ -53,68 +57,94 @@ public class MyTracksOverlay extends Overlay {
private final Paint selectedTrackPaint; private final Paint selectedTrackPaint;
private final Paint errorCirclePaint; private final Paint errorCirclePaint;
private final Context context; private final Context context;
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>(); private final List<Waypoint> waypoints;
private final ArrayList<Location> points = new ArrayList<Location>(1024); private final List<CachedLocation> points;
private final BlockingQueue<CachedLocation> pendingPoints;
private boolean trackDrawingEnabled; private boolean trackDrawingEnabled;
private int lastHeading = 0; private int lastHeading = 0;
private Location myLocation; private Location myLocation;
private boolean showEndMarker = true; private boolean showEndMarker = true;
private boolean drawBounds; // TODO: Remove it completely after completing performance tests.
private boolean alwaysVisible = true;
private GeoPoint lastReferencePoint;
private Rect lastViewRect;
private Path lastPath;
/**
* Represents a pre-processed {@code Location} to speed up drawing.
* This class is more like a data object and doesn't provide accessors.
*/
private static class CachedLocation {
public final boolean valid;
public final GeoPoint geoPoint;
public CachedLocation(Location location) {
this.valid = MyTracksUtils.isValidLocation(location);
this.geoPoint = valid ? MyTracksUtils.getGeoPoint(location) : null;
}
};
public MyTracksOverlay(Context context) { public MyTracksOverlay(Context context) {
this.context = context; this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true);
// TODO: Can we use a FrameAnimation or similar here rather // TODO: Can we use a FrameAnimation or similar here rather
// than individual resources for each arrow direction? // than individual resources for each arrow direction?
final Resources resources = context.getResources();
arrow[0] = context.getResources().getDrawable(R.drawable.arrow_0); arrows = new Drawable[] {
arrow[1] = context.getResources().getDrawable(R.drawable.arrow_20); resources.getDrawable(R.drawable.arrow_0),
arrow[2] = context.getResources().getDrawable(R.drawable.arrow_40); resources.getDrawable(R.drawable.arrow_20),
arrow[3] = context.getResources().getDrawable(R.drawable.arrow_60); resources.getDrawable(R.drawable.arrow_40),
arrow[4] = context.getResources().getDrawable(R.drawable.arrow_80); resources.getDrawable(R.drawable.arrow_60),
arrow[5] = context.getResources().getDrawable(R.drawable.arrow_100); resources.getDrawable(R.drawable.arrow_80),
arrow[6] = context.getResources().getDrawable(R.drawable.arrow_120); resources.getDrawable(R.drawable.arrow_100),
arrow[7] = context.getResources().getDrawable(R.drawable.arrow_140); resources.getDrawable(R.drawable.arrow_120),
arrow[8] = context.getResources().getDrawable(R.drawable.arrow_160); resources.getDrawable(R.drawable.arrow_140),
arrow[9] = context.getResources().getDrawable(R.drawable.arrow_180); resources.getDrawable(R.drawable.arrow_160),
arrow[10] = context.getResources().getDrawable(R.drawable.arrow_200); resources.getDrawable(R.drawable.arrow_180),
arrow[11] = context.getResources().getDrawable(R.drawable.arrow_220); resources.getDrawable(R.drawable.arrow_200),
arrow[12] = context.getResources().getDrawable(R.drawable.arrow_240); resources.getDrawable(R.drawable.arrow_220),
arrow[13] = context.getResources().getDrawable(R.drawable.arrow_260); resources.getDrawable(R.drawable.arrow_240),
arrow[14] = context.getResources().getDrawable(R.drawable.arrow_280); resources.getDrawable(R.drawable.arrow_260),
arrow[15] = context.getResources().getDrawable(R.drawable.arrow_300); resources.getDrawable(R.drawable.arrow_280),
arrow[16] = context.getResources().getDrawable(R.drawable.arrow_320); resources.getDrawable(R.drawable.arrow_300),
arrow[17] = context.getResources().getDrawable(R.drawable.arrow_340); resources.getDrawable(R.drawable.arrow_320),
arrowWidth = arrow[lastHeading].getIntrinsicWidth(); resources.getDrawable(R.drawable.arrow_340)
arrowHeight = arrow[lastHeading].getIntrinsicHeight(); };
for (int i = 0; i <= 17; i++) { arrowWidth = arrows[lastHeading].getIntrinsicWidth();
arrow[i].setBounds(0, 0, arrowWidth, arrowHeight); arrowHeight = arrows[lastHeading].getIntrinsicHeight();
for (Drawable arrow : arrows) {
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
} }
statsMarker = context.getResources().getDrawable(R.drawable.ylw_pushpin); statsMarker = resources.getDrawable(R.drawable.ylw_pushpin);
markerWidth = statsMarker.getIntrinsicWidth(); markerWidth = statsMarker.getIntrinsicWidth();
markerHeight = statsMarker.getIntrinsicHeight(); markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight); statsMarker.setBounds(0, 0, markerWidth, markerHeight);
startMarker = context.getResources().getDrawable(R.drawable.green_dot); startMarker = resources.getDrawable(R.drawable.green_dot);
startMarker.setBounds(0, 0, markerWidth, markerHeight); startMarker.setBounds(0, 0, markerWidth, markerHeight);
endMarker = context.getResources().getDrawable(R.drawable.red_dot); endMarker = resources.getDrawable(R.drawable.red_dot);
endMarker.setBounds(0, 0, markerWidth, markerHeight); endMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
context.getResources().getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight); waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
selectedTrackPaint = new Paint(); selectedTrackPaint = new Paint();
selectedTrackPaint.setColor(context.getResources().getColor(R.color.red)); selectedTrackPaint.setColor(resources.getColor(R.color.red));
selectedTrackPaint.setStrokeWidth(3); selectedTrackPaint.setStrokeWidth(3);
selectedTrackPaint.setStyle(Paint.Style.STROKE); selectedTrackPaint.setStyle(Paint.Style.STROKE);
selectedTrackPaint.setAntiAlias(true); selectedTrackPaint.setAntiAlias(true);
errorCirclePaint = new Paint(); errorCirclePaint = new Paint();
errorCirclePaint.setColor(context.getResources().getColor(R.color.blue)); errorCirclePaint.setColor(resources.getColor(R.color.blue));
errorCirclePaint.setStyle(Paint.Style.STROKE); errorCirclePaint.setStyle(Paint.Style.STROKE);
errorCirclePaint.setStrokeWidth(3); errorCirclePaint.setStrokeWidth(3);
errorCirclePaint.setAlpha(127); errorCirclePaint.setAlpha(127);
@@ -124,35 +154,50 @@ public class MyTracksOverlay extends Overlay {
/** /**
* Add a location to the map overlay. * Add a location to the map overlay.
* *
* NOTE: this method takes ownership of this location and may change it. * NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method.
* *
* @param l the location to add * @param l the location to add.
*/ */
public void addLocation(Location l) { public void addLocation(Location l) {
if (l != null) { // Queue up in the pending queue until it's merged with {@code #points}.
synchronized (points) { pendingPoints.offer(new CachedLocation(l));
points.add(l); }
public void addWaypoint(Waypoint wpt) {
// Note: We don't cache waypoints, because it's not worth the effort.
if (wpt != null && wpt.getLocation() != null) {
synchronized (waypoints) {
waypoints.add(wpt);
} }
} }
} }
public void addWaypoint(Waypoint wpt) {
waypoints.add(wpt);
}
public int getNumLocations() { public int getNumLocations() {
synchronized (points) { synchronized (points) {
return points.size(); return points.size() + pendingPoints.size();
} }
} }
public void clearWaypoints() { // Visible for testing.
waypoints.clear(); int getNumWaypoints() {
synchronized (waypoints) {
return waypoints.size();
}
} }
public void clearPoints() { public void clearPoints() {
synchronized (points) { synchronized (points) {
points.clear(); points.clear();
pendingPoints.clear();
lastPath = null;
lastViewRect = null;
}
}
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
} }
} }
@@ -170,230 +215,175 @@ public class MyTracksOverlay extends Overlay {
return; return;
} }
// It's safe to keep projection within a single draw operation.
final Projection projection = getMapProjection(mapView);
// Get the current viewing window.
if (trackDrawingEnabled) { if (trackDrawingEnabled) {
Rect viewRect = getMapViewRect(mapView);
// Draw the selected track: // Draw the selected track:
drawTrack(canvas, mapView); drawTrack(canvas, projection, viewRect);
// Draw the waypoints: // Draw the waypoints:
drawWaypoints(canvas, mapView); drawWaypoints(canvas, projection);
} }
// Draw the current location // Draw the current location
drawMyLocation(canvas, mapView); drawMyLocation(canvas, projection);
} }
private void drawWaypoints(Canvas canvas, MapView mapView) { // Visible for testing.
ArrayList<Waypoint> currentWaypoints = waypoints; Projection getMapProjection(MapView mapView) {
for (int i = 1; i < currentWaypoints.size(); i++) { return mapView.getProjection();
Waypoint wpt = currentWaypoints.get(i);
if (wpt == null) {
continue;
}
Location loc = wpt.getLocation();
if (loc == null) {
continue;
}
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc);
Point pt = new Point();
mapView.getProjection().toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x - (markerWidth / 2) + 3, pt.y - (markerHeight));
if (wpt.getType() == Waypoint.TYPE_STATISTICS) {
statsMarker.draw(canvas);
} else {
waypointMarker.draw(canvas);
}
canvas.restore();
}
} }
private void drawMyLocation(Canvas canvas, MapView mapView) { // Visible for testing.
// Draw the arrow icon: Rect getMapViewRect(MapView mapView) {
if (myLocation == null) {
return;
}
GeoPoint geoPoint = new GeoPoint(
(int) (myLocation.getLatitude() * 1E6),
(int) (myLocation.getLongitude() * 1E6));
Point pt = new Point();
mapView.getProjection().toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x - (arrowWidth / 2), pt.y - (arrowHeight / 2));
arrow[lastHeading].draw(canvas);
canvas.restore();
// Draw the error circle:
float radius =
mapView.getProjection().metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
}
private void drawTrack(Canvas canvas, MapView mapView) {
Path path;
Point pt = new Point();
Location lastValidLocation;
int locLon = 0, locLat = 0;
GeoPoint firstGeoPoint = null;
boolean lastLocValid;
// Get the current viewing window:
int w = mapView.getLongitudeSpan(); int w = mapView.getLongitudeSpan();
int h = mapView.getLatitudeSpan(); int h = mapView.getLatitudeSpan();
int cx = mapView.getMapCenter().getLongitudeE6(); int cx = mapView.getMapCenter().getLongitudeE6();
int cy = mapView.getMapCenter().getLatitudeE6(); int cy = mapView.getMapCenter().getLatitudeE6();
Rect viewRect = new Rect(cx - w, cy - h, cx + w, cy + h); return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
}
// Global bounding box, including points not visible // Visible for testing.
int allMinLat, allMinLon, allMaxLat, allMaxLon; Path newPath() {
return new Path();
}
// Visible for testing.
Path getLastPath() {
return lastPath;
}
private void drawWaypoints(Canvas canvas, Projection projection) {
synchronized (waypoints) {;
for (Waypoint wpt : waypoints) {
Location loc = wpt.getLocation();
drawElement(canvas, projection, MyTracksUtils.getGeoPoint(loc),
wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
: waypointMarker, -(markerWidth / 2) + 3, -markerHeight);
}
}
}
private void drawMyLocation(Canvas canvas, Projection projection) {
// Draw the arrow icon.
if (myLocation == null) {
return;
}
Point pt = drawElement(canvas, projection,
MyTracksUtils.getGeoPoint(myLocation), arrows[lastHeading],
-(arrowWidth / 2) + 3, -(arrowHeight / 2));
// Draw the error circle.
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
}
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) {
Path path;
synchronized (points) { synchronized (points) {
int numPoints = points.size(); // Merge the pending points with the list of cached locations.
if (numPoints < 2) { final GeoPoint referencePoint = projection.fromPixels(0, 0);
return; int newPoints = pendingPoints.drainTo(points);
} boolean newProjection = !viewRect.equals(lastViewRect) ||
!referencePoint.equals(lastReferencePoint);
GeoPoint geoPoint; if (newPoints == 0 && lastPath != null && !newProjection) {
Location loc; // No need to recreate path (same points and viewing area).
int minLon, maxLon, minLat, maxLat; path = lastPath;
lastValidLocation = points.get(0); } else {
int lastLocLon = allMinLon = allMaxLon = (int) (lastValidLocation.getLongitude() * 1E6); int numPoints = points.size();
int lastLocLat = allMinLat = allMaxLat = (int) (lastValidLocation.getLatitude() * 1E6); if (numPoints < 2) {
lastLocValid = MyTracksUtils.isValidLocation(lastValidLocation); // Not enough points to draw a path.
boolean lastLocVisible = false; path = null;
path = new Path(); } else if (lastPath != null && !newProjection) {
// Incremental update of the path, without repositioning the view.
// Loop over track points: path = lastPath;
path.incReserve(numPoints); updatePath(projection, viewRect, path, numPoints - newPoints);
for (int i = 1; i < numPoints; i++) { } else {
loc = points.get(i); // The view has changed so we have to start from scratch.
path = newPath();
boolean locValid = MyTracksUtils.isValidLocation(loc); path.incReserve(numPoints);
boolean locVisible = false; updatePath(projection, viewRect, path, 0);
if (locValid) { }
locLon = (int) (loc.getLongitude() * 1E6); lastPath = path;
locLat = (int) (loc.getLatitude() * 1E6); }
lastReferencePoint = referencePoint;
if (firstGeoPoint == null) { lastViewRect = viewRect;
// Found the starting point }
firstGeoPoint = new GeoPoint(locLat, locLon); if (path != null) {
} canvas.drawPath(path, selectedTrackPaint);
}
// If both the current and previous locations were valid
if (lastLocValid) { // Draw the "End" marker.
lastValidLocation = loc; if (showEndMarker) {
for (int i = points.size() - 1; i >= 0; --i) {
// Get the bounding box of the segment about to be drawn if (points.get(i).valid) {
if (locLon > lastLocLon) { drawElement(canvas, projection, points.get(i).geoPoint, endMarker,
minLon = lastLocLon; -markerWidth / 2, -markerHeight);
maxLon = locLon; break;
} else {
minLon = locLon;
maxLon = lastLocLon;
}
if (locLat > lastLocLat) {
minLat = lastLocLat;
maxLat = locLat;
} else {
minLat = locLat;
maxLat = lastLocLat;
}
if (drawBounds) {
allMaxLat = Math.max(allMaxLat, maxLat);
allMinLat = Math.min(allMinLat, minLat);
allMaxLon = Math.max(allMaxLon, maxLon);
allMinLon = Math.min(allMinLon, minLon);
}
// See if that bounding box intersects the viewable bounding box
// Assume that if it does, the location is visible
locVisible = viewRect.intersects(minLon, minLat, maxLon, maxLat);
if (locVisible) {
// If the previous point wasn't drawn, start at its position
if (!lastLocVisible) {
geoPoint = new GeoPoint(lastLocLat, lastLocLon);
mapView.getProjection().toPixels(geoPoint, pt);
path.moveTo(pt.x, pt.y);
}
// Draw a line to the new point
geoPoint = new GeoPoint(locLat, locLon);
mapView.getProjection().toPixels(geoPoint, pt);
path.lineTo(pt.x, pt.y);
}
}
} }
lastLocLon = locLon;
lastLocLat = locLat;
lastLocValid = locValid;
lastLocVisible = locVisible;
} }
} }
canvas.drawPath(path, selectedTrackPaint); // Draw the "Start" marker.
for (int i = 0; i < points.size(); ++i) {
if (drawBounds) { if (points.get(i).valid) {
drawBoundingBox(canvas, mapView, drawElement(canvas, projection, points.get(i).geoPoint, startMarker,
allMinLat, allMinLon, allMaxLat, allMaxLon); -markerWidth / 2, -markerHeight);
} break;
}
// Draw the "End" marker:
if (showEndMarker && lastValidLocation != null) {
canvas.save();
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(lastValidLocation);
mapView.getProjection().toPixels(geoPoint, pt);
canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight);
endMarker.draw(canvas);
canvas.restore();
}
// Draw the "Start" marker:
if (firstGeoPoint != null) {
mapView.getProjection().toPixels(firstGeoPoint, pt);
canvas.save();
canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight);
startMarker.draw(canvas);
canvas.restore();
} }
} }
private void drawBoundingBox(Canvas canvas, MapView mapView, int allMinLat, private void updatePath(Projection projection, Rect viewRect, Path path,
int allMinLon, int allMaxLat, int allMaxLon) { int startLocationIdx) {
// Transform coordinates // Whether to start a new segment on new valid and visible point.
GeoPoint maxPoint = new GeoPoint(allMaxLat, allMaxLon); boolean newSegment =
GeoPoint minPoint = new GeoPoint(allMinLat, allMinLon); startLocationIdx > 0 ? !points.get(startLocationIdx - 1).valid : true;
Point minPt = new Point(); boolean lastVisible = !newSegment;
Point maxPt = new Point(); final Point pt = new Point();
mapView.getProjection().toPixels(minPoint, minPt); // Loop over track points.
mapView.getProjection().toPixels(maxPoint, maxPt); for (int i = startLocationIdx; i < points.size(); ++i) {
Rect allBounds = new Rect(minPt.x, minPt.y, maxPt.x, maxPt.y); CachedLocation loc = points.get(i);
// Check if valid, if not then indicate a new segment.
if (!loc.valid) {
newSegment = true;
continue;
}
// Prepare a green paint final GeoPoint geoPoint = loc.geoPoint;
Paint boundingBoxPaint = new Paint(); // Check if this breaks the existing segment.
boundingBoxPaint.setColor(context.getResources().getColor(R.color.green)); boolean visible = alwaysVisible || viewRect.contains(
boundingBoxPaint.setStrokeWidth(2); geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
boundingBoxPaint.setStyle(Paint.Style.STROKE); if (!visible && !lastVisible) {
boundingBoxPaint.setAntiAlias(true); // This is a point outside view not connected to a visible one.
newSegment = true;
}
lastVisible = visible;
// Disable clipping // Either move to beginning of a new segment or continue the old one.
projection.toPixels(geoPoint, pt);
if (newSegment) {
path.moveTo(pt.x, pt.y);
newSegment = false;
} else {
path.lineTo(pt.x, pt.y);
}
}
}
// Visible for testing.
Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
Drawable element, int offsetX, int offsetY) {
Point pt = new Point();
projection.toPixels(geoPoint, pt);
canvas.save(); canvas.save();
canvas.clipRect(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, canvas.translate(pt.x + offsetX, pt.y + offsetY);
Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, element.draw(canvas);
Region.Op.REPLACE);
// Draw the bounding box
canvas.drawRect(allBounds, boundingBoxPaint);
// Re-enable clipping
canvas.restore(); canvas.restore();
} return pt;
@Override
public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
long when) {
draw(canvas, mapView, shadow);
return false;
} }
/** /**
@@ -436,20 +426,22 @@ public class MyTracksOverlay extends Overlay {
final Location tapLocation = MyTracksUtils.getLocation(p); final Location tapLocation = MyTracksUtils.getLocation(p);
double dmin = Double.MAX_VALUE; double dmin = Double.MAX_VALUE;
Waypoint waypoint = null; Waypoint waypoint = null;
for (int i = 0; i < waypoints.size(); i++) { synchronized (waypoints) {
final Location waypointLocation = waypoints.get(i).getLocation(); for (int i = 0; i < waypoints.size(); i++) {
if (waypointLocation == null) { final Location waypointLocation = waypoints.get(i).getLocation();
continue; if (waypointLocation == null) {
} continue;
final double d = waypointLocation.distanceTo(tapLocation); }
if (d < dmin) { final double d = waypointLocation.distanceTo(tapLocation);
dmin = d; if (d < dmin) {
waypoint = waypoints.get(i); dmin = d;
waypoint = waypoints.get(i);
}
} }
} }
if (waypoint != null if (waypoint != null &&
&& dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
Intent intent = new Intent(context, MyTracksWaypointDetails.class); Intent intent = new Intent(context, MyTracksWaypointDetails.class);
intent.putExtra("waypointid", waypoint.getId()); intent.putExtra("waypointid", waypoint.getId());
context.startActivity(intent); context.startActivity(intent);
@@ -457,8 +449,4 @@ public class MyTracksOverlay extends Overlay {
} }
return super.onTap(p, mapView); return super.onTap(p, mapView);
} }
public void setDrawBounds(boolean drawBounds) {
this.drawBounds = drawBounds;
}
} }
@@ -17,7 +17,8 @@ package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper; import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.SafeStatusAnnouncerTask; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@@ -25,12 +26,10 @@ import android.os.Bundle;
import android.preference.CheckBoxPreference; import android.preference.CheckBoxPreference;
import android.preference.ListPreference; import android.preference.ListPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener; import android.preference.Preference.OnPreferenceClickListener;
import android.speech.tts.TextToSpeech; import android.preference.PreferenceActivity;
import android.util.Log; import android.preference.PreferenceManager;
/** /**
* An activity that let's the user see and edit the settings. * An activity that let's the user see and edit the settings.
@@ -53,29 +52,19 @@ public class MyTracksSettings extends PreferenceActivity {
public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200; public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200;
public static final int DEFAULT_SPLIT_FREQUENCY = 0; public static final int DEFAULT_SPLIT_FREQUENCY = 0;
private static boolean mTTSAvailable;
private BackupPreferencesListener backupListener; private BackupPreferencesListener backupListener;
private SharedPreferences preferences; private SharedPreferences preferences;
/* establish whether the tts class is available to us */
static {
try {
SafeStatusAnnouncerTask.checkAvailable();
mTTSAvailable = true;
} catch (Throwable t) {
Log.d(MyTracksConstants.TAG, "TTS not available.", t);
mTTSAvailable = false;
}
}
/** Called when the activity is first created. */ /** Called when the activity is first created. */
@Override @Override
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
// Tell it where to read/write preferences // Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager(); PreferenceManager preferenceManager = getPreferenceManager();
@@ -83,7 +72,7 @@ public class MyTracksSettings extends PreferenceActivity {
preferenceManager.setSharedPreferencesMode(0); preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup // Set up automatic preferences backup
backupListener = BackupPreferencesListener.create(this); backupListener = BackupPreferencesListener.create(this, apiFeatures);
preferences = preferenceManager.getSharedPreferences(); preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener); preferences.registerOnSharedPreferenceChangeListener(backupListener);
@@ -108,7 +97,7 @@ public class MyTracksSettings extends PreferenceActivity {
updatePreferenceUnits(metricUnitsPreference.isChecked()); updatePreferenceUnits(metricUnitsPreference.isChecked());
// Disable TTS announcement preference if not available // Disable TTS announcement preference if not available
if (!mTTSAvailable) { if (!apiFeatures.hasTextToSpeech()) {
IntegerListPreference announcementFrequency = IntegerListPreference announcementFrequency =
(IntegerListPreference) findPreference( (IntegerListPreference) findPreference(
getString(R.string.announcement_frequency_key)); getString(R.string.announcement_frequency_key));
@@ -23,7 +23,8 @@ package com.google.android.apps.mytracks;
*/ */
public interface ProgressIndicator { public interface ProgressIndicator {
public void setProgressMessage(String text); public void setProgressMessage(int resId);
public void clearProgressMessage();
public void setProgressValue(int percent); public void setProgressValue(int percent);
@@ -20,6 +20,8 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtilsImpl;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
@@ -33,7 +35,6 @@ import android.location.LocationManager;
import android.location.LocationProvider; import android.location.LocationProvider;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.speech.tts.TextToSpeech;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
@@ -143,7 +144,9 @@ public class StatsActivity extends Activity
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -175,7 +175,7 @@ public interface MyTracksProviderUtils {
* @return A cursor over the selected range of locations * @return A cursor over the selected range of locations
*/ */
Cursor getWaypointsCursor(long trackId, long minWaypointId, Cursor getWaypointsCursor(long trackId, long minWaypointId,
long maxWaypoints); int maxWaypoints);
/** /**
* Finds a track by given unique track id. * Finds a track by given unique track id.
@@ -206,32 +206,6 @@ public interface MyTracksProviderUtils {
*/ */
long getTrackPoints(Track track, int maxPoints); long getTrackPoints(Track track, int maxPoints);
/**
* Fetches some number of locations for the given track.
*
* This is designed to be used to stream through large tracks without loading
* all points into memory.
*
* @param track to load locations for
* @param buffer an array of locations to fill
* @deprecated use {@link #fillTrackPoints} instead
*/
@Deprecated
void getTrackPoints(Track track, TrackBuffer buffer);
/**
* Fetches some number of locations for the given track.
*
* This is designed to be used to stream through large tracks without loading
* all points into memory.
* This method will reuse the Location objects in the buffer. If you need a
* Location object copy the object.
*
* @param track to load locations for
* @param buffer an array of locations to fill
*/
void fillTrackPoints(Track track, TrackBuffer buffer);
/** /**
* Creates a cursor over the tracks provider with a given selection. * Creates a cursor over the tracks provider with a given selection.
* Caller gets to own the returned cursor. Don't forget to close it. * Caller gets to own the returned cursor. Don't forget to close it.
@@ -775,7 +775,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override @Override
public Cursor getWaypointsCursor(long trackId, long minWaypointId, public Cursor getWaypointsCursor(long trackId, long minWaypointId,
long maxWaypoints) { int maxWaypoints) {
String selection; String selection;
if (minWaypointId > 0) { if (minWaypointId > 0) {
selection = String.format("%s=%d AND %s>=%d", selection = String.format("%s=%d AND %s>=%d",
@@ -845,67 +845,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
return lastId; return lastId;
} }
@Override
public void getTrackPoints(Track track, TrackBuffer buffer) {
getTrackPoints(track, buffer, false);
}
@Override
public void fillTrackPoints(Track track, TrackBuffer buffer) {
getTrackPoints(track, buffer, true);
}
public void getTrackPoints(Track track, TrackBuffer buffer,
boolean reuseLocations) {
long startingPoint = buffer.getLastLocationRead() == 0 ? track.getStartId()
: buffer.getLastLocationRead();
buffer.reset();
Cursor cursor = getLocationsCursor(track.getId(),
startingPoint,
buffer.getSize(), false);
if (cursor == null) {
Log.w(MyTracksProvider.TAG, "Cannot get a locations cursor!");
buffer.setInvalid();
return;
}
try {
if (cursor.getCount() == 0) {
Log.w(MyTracksProvider.TAG, "No matching locations found.");
buffer.resetAt(startingPoint + buffer.getSize());
return;
}
if (!cursor.moveToFirst()) {
Log.w(MyTracksProvider.TAG, "Could not move to first.");
buffer.setInvalid();
return;
}
final int idColumnIdx =
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
do {
if (reuseLocations) {
fillLocation(cursor, buffer.add(cursor.getLong(idColumnIdx)));
} else {
Location location = createLocation(cursor);
if (location == null) {
continue;
}
buffer.add(location, cursor.getLong(idColumnIdx));
}
} while (cursor.moveToNext());
if (buffer.getLocationsLoaded() == 0) {
Log.w(MyTracksProvider.TAG, "No locations read.");
buffer.resetAt(startingPoint + buffer.getSize());
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e);
} finally {
cursor.close();
}
}
@Override @Override
public Cursor getTracksCursor(String selection) { public Cursor getTracksCursor(String selection) {
Cursor cursor = context.getContentResolver().query( Cursor cursor = context.getContentResolver().query(
@@ -1,149 +0,0 @@
/*
* 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.content;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import android.location.Location;
/**
* A buffer of Locations from a track.
* This class contains some additional information about the loaded points.
*
* @author Sandor Dornbush
*/
public class TrackBuffer {
/**
* An array of location objects that are part of a track.
*/
private final Location[] buffer;
/**
* The id of the last location read from the track.
*/
private long lastLocationRead = 0;
/**
* The number of locations loaded into the buffer.
*/
private int locationsLoaded = 0;
/**
* Create a TrackBuffer with size elements.
*
* @param size The size of the buffer
*/
public TrackBuffer(int size) {
buffer = new Location[size];
}
/**
* Reset the track to a state with no locations.
*/
public void reset() {
resetAt(0);
}
/**
* Reset the track at the given starting location id.
*/
public void resetAt(long lastLocation) {
lastLocationRead = lastLocation;
locationsLoaded = 0;
}
public void setInvalid() {
lastLocationRead = Integer.MAX_VALUE;
}
/**
* @return The number of locations that can be stored in this buffer
*/
public int getSize() {
return buffer.length;
}
/**
* @param index The index of the location to fetch
* @return The location for the given index
*/
public Location get(int index) {
return buffer[index];
}
/**
* Adds a location to the end of the buffer.
* @param location The location to add.
* @param id The id of the location to be added.
*/
public void add(Location location, long id) {
buffer[locationsLoaded++] = location;
lastLocationRead = Math.max(lastLocationRead, id);
}
/**
* Get a location from the end of the buffer.
* This may reuse Location objects already allocated to the buffer.
* @param locationId The of the location to be added.
* @return The location object associated with the give id.
*/
public Location add(long locationId) {
lastLocationRead = Math.max(lastLocationRead, locationId);
if (buffer[locationsLoaded] == null) {
buffer[locationsLoaded] = new Location("");
} else {
buffer[locationsLoaded].reset();
}
return buffer[locationsLoaded++];
}
/**
* @return The id of the last location loaded into the buffer
*/
public long getLastLocationRead() {
return lastLocationRead;
}
/**
* @return The number of locations loaded into the buffer.
*/
public int getLocationsLoaded() {
return locationsLoaded;
}
/**
* Finds the start location, i.e. the one which is the first point of a
* segment with at least two points.
*
* @return the start location
*/
public Location findStartLocation() {
int numValidLocations = 0;
for (int i = 0; i < getLocationsLoaded(); i++) {
Location location = buffer[i];
if (MyTracksUtils.isValidLocation(location)) {
numValidLocations++;
if (numValidLocations == 2) {
return buffer[i - 1];
}
} else {
numValidLocations = 0;
}
}
return null;
}
}
@@ -25,7 +25,6 @@ import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.ProgressIndicator; import com.google.android.apps.mytracks.ProgressIndicator;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackBuffer;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
@@ -44,53 +43,392 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log; import android.util.Log;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Vector; import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/** /**
* A helper class used to transmit tracks to Google MyMaps. * A helper class used to transmit tracks to Google MyMaps.
* A new instance should be used for each upload.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class SendToMyMaps { public class SendToMyMaps implements Runnable {
public static final String NEW_MAP_ID = "new";
private static final String END_ICON_URL = private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png"; "http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final String START_ICON_URL = private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png"; "http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private final Activity context; private final Activity context;
private final AuthManager auth; private final AuthManager auth;
private final long trackId; private final long trackId;
private final ProgressIndicator progressIndicator; private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils; private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils; private final MyTracksProviderUtils providerUtils;
private String mapId; private String mapId;
private String statusMessage = ""; // GData service
private boolean success = true;
private Runnable onCompletion;
private HandlerThread handlerThread;
private Handler handler;
private String originalDescription;
private MyMapsGDataWrapper wrapper; private MyMapsGDataWrapper wrapper;
private MyMapsGDataConverter gdataConverter; private MyMapsGDataConverter gdataConverter;
private Location last = null; // Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
private Vector<Double> distances; public interface OnSendCompletedListener {
private Vector<Double> elevations; void onSendCompleted(String mapId, boolean success, int statusMessage);
private DoubleBuffer elevationBuffer; }
public SendToMyMaps(Activity context, String mapId, AuthManager auth,
long trackId, ProgressIndicator progressIndicator,
OnSendCompletedListener onCompletion) {
this.context = context;
this.mapId = mapId;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public void run() {
Log.d(MyTracksConstants.TAG, "Sending to MyMaps: trackId = " + trackId);
doUpload();
}
private void doUpload() {
int statusMessageId = R.string.error_sending_to_mymap;
boolean success = true;
try {
gdataConverter = new MyMapsGDataConverter();
progressIndicator.setProgressValue(1);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
String originalDescription = track.getDescription();
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
wrapper = new MyMapsGDataWrapper(context);
wrapper.setAuthManager(auth);
wrapper.setRetryOnAuthFailure(true);
// Create a new map if necessary:
boolean isNewMap = mapId.equals(NEW_MAP_ID);
if (isNewMap) {
success = createNewMap(track, originalDescription);
}
// Upload all of the segments of the track plus start/end markers
if (success) {
success = uploadAllTrackPoints(track, originalDescription);
}
// Put waypoints.
if (success) {
success = uploadWaypoints(track);
if (!success) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: upload waypoints failed.");
}
}
if (success) {
statusMessageId = isNewMap
? R.string.status_new_mymap_has_been_created
: R.string.status_tracks_have_been_uploaded;
}
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Done: " + success);
progressIndicator.setProgressValue(100);
} catch (XmlPullParserException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
} finally {
if (wrapper != null) {
wrapper.cleanUp();
}
final boolean finalSuccess = success;
final int finalStatusMessageId = statusMessageId;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(
mapId, finalSuccess, finalStatusMessageId);
}
}
});
}
}
/**
* Creates a new map for the given track.
*
* @param track The track that will be uploaded to this map
* @return True on success.
*/
private boolean createNewMap(final Track track, final String description) {
progressIndicator.setProgressMessage(
R.string.progress_message_creating_map);
return wrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
Log.d(MyTracksConstants.TAG, "Creating a new map.");
String mapFeed = MapsClient.getMapsFeed();
Log.d(MyTracksConstants.TAG, "Map feed is " + mapFeed);
MyMapsMapMetadata metaData = new MyMapsMapMetadata();
metaData.setTitle(track.getName());
metaData.setDescription(description + " - "
+ track.getCategory() + " - "
+ context.getString(R.string.new_map_description));
SharedPreferences preferences = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
boolean mapPublic = true;
if (preferences != null) {
mapPublic = preferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
}
metaData.setSearchable(mapPublic);
Entry entry = MyMapsGDataConverter.getMapEntryForMetadata(metaData);
Log.d(MyTracksConstants.TAG, "Title: " + entry.getTitle());
Entry map = client.createEntry(mapFeed, auth.getAuthToken(), entry);
mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
Log.d(MyTracksConstants.TAG, "New map id is: " + mapId);
}
});
}
private boolean uploadAllTrackPoints(
final Track track, String originalDescription) {
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);
}
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (!locationsCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(MyTracksConstants.TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer =
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
uploadMarker(track, loc, true);
}
// Add to the elevation profile.
if (loc != null && MyTracksUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
== MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "</p>");
return uploadMarker(track, lastLocation, false);
}
return true;
} finally {
locationsCursor.close();
}
}
private boolean uploadMarker(final Track track,
final Location location,
final boolean isStart) {
boolean okay = wrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client)
throws AuthenticationException, IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
insertMarker(context, client, featureFeed, track, location, isStart);
}
});
return okay;
}
/**
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
* 2nd failure.
*/
private void insertMarker(Context context, MapsClient client,
String featureFeed, Track track, Location loc,
boolean isStart) throws IOException, Exception {
Entry entry = gdataConverter.getEntryForFeature(
buildMyMapsPlacemarkFeature(context, track, loc, isStart));
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Creating placemark "
+ entry.getTitle());
try {
client.createEntry(featureFeed, auth.getAuthToken(), entry);
Log.d(MyTracksConstants.TAG, "SendToMyMaps: createEntry success!");
} catch (IOException e) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, auth.getAuthToken(), entry);
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: createEntry success on 2nd try!");
}
}
/**
* Builds a placemark MyMapsFeature from a track.
*
* @param track the track
* @param isStart true if it's the start of the track, or false for end
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsPlacemarkFeature(Context context,
Track track, Location loc, boolean isStart) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.MARKER);
if (isStart) {
myMapsFeature.setIconUrl(START_ICON_URL);
} else {
myMapsFeature.setIconUrl(END_ICON_URL);
}
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
String name = track.getName() + " "
+ (isStart ? context.getString(R.string.start)
: context.getString(R.string.end));
myMapsFeature.setTitle(name);
myMapsFeature.setDescription(isStart ? "" : track.getDescription());
return myMapsFeature;
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
progressIndicator.setProgressMessage(
R.string.progress_message_preparing_track);
updateProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
progressIndicator.setProgressMessage(
R.string.progress_message_sending_mymaps);
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.part), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth);
if (!uploadTrackPoints(splitTrack)) {
Log.e(MyTracksConstants.TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateProgress();
return true;
}
/** /**
* Prepares a buffer of locations for transmission to google maps. * Prepares a buffer of locations for transmission to google maps.
@@ -100,8 +438,8 @@ public class SendToMyMaps {
* @return an array of tracks each with a sub section of the points in the * @return an array of tracks each with a sub section of the points in the
* original buffer * original buffer
*/ */
private ArrayList<Track> prepareLocations(Track track, private ArrayList<Track> prepareLocations(
TrackBuffer buffer) { Track track, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>(); ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track: // Create segments from each full track:
@@ -115,8 +453,10 @@ public class SendToMyMaps {
segmentStats.setStartTime(trackStats.getStartTime()); segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime()); segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false; boolean startNewTrackSegment = false;
for (int i = 0; i < buffer.getLocationsLoaded(); ++i) { for (Location loc : locations) {
Location loc = buffer.get(i); if (totalLocationsPrepared % 100 == 0) {
updateProgress();
}
if (loc.getLatitude() > 90) { if (loc.getLatitude() > 90) {
startNewTrackSegment = true; startNewTrackSegment = true;
} }
@@ -141,6 +481,7 @@ public class SendToMyMaps {
segmentStats.setStartTime(loc.getTime()); segmentStats.setStartTime(loc.getTime());
} }
} }
totalLocationsPrepared++;
} }
prepareTrackSegment(segment, splitTracks); prepareTrackSegment(segment, splitTracks);
@@ -182,237 +523,30 @@ public class SendToMyMaps {
} }
} }
/**
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
* 2nd failure.
*/
private void insertMarker(Context context, MapsClient client,
String featureFeed, Track track, Location loc,
boolean isStart) throws IOException, Exception {
Entry entry = gdataConverter.getEntryForFeature(
buildMyMapsPlacemarkFeature(context, track, loc, isStart));
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Creating placemark "
+ entry.getTitle());
try {
client.createEntry(featureFeed, auth.getAuthToken(), entry);
Log.d(MyTracksConstants.TAG, "SendToMyMaps: createEntry success!");
} catch (IOException e) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, auth.getAuthToken(), entry);
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: createEntry success on 2nd try!");
}
}
private boolean uploadMarker(final Track track,
final Location location,
final boolean isStart) {
boolean okay = wrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client)
throws AuthenticationException, IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
insertMarker(context, client, featureFeed, track, location, isStart);
}
});
return okay;
}
/**
* Sets the current upload progress.
*
* @param track The track being uploaded
* @param totalLocationsRead The total number of locations already read and
* uploaded
* @param segmentSize The percent of total that each segment corresponds to
* @param progressInSegment The approximate progress in this segment
*/
private void setProgress(final Track track, int totalLocationsRead,
double segmentSize, double progressInSegment) {
long totalLocations = track.getStopId() - track.getStartId();
// The percent of the total that represents the completed part of this
// segment.
double percentSegmentInTotal = (segmentSize * progressInSegment);
int progress = (int) ((100.0 * totalLocationsRead / totalLocations)
+ percentSegmentInTotal);
progress = Math.min(99, progress);
progressIndicator.setProgressValue(progress);
}
private boolean uploadAllTrackPoints(final Track track) {
int totalLocationsRead = 0;
// TODO: This might not actually be accurate.
long totalLocations = track.getStopId() - track.getStartId();
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(MyTracksConstants.TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Location lastLocation = null;
TrackBuffer buffer = new TrackBuffer((int) Math.min(2048, totalLocations));
int part = 1;
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);
}
// For the progress monitor:
// For each track segment 33% = read, 33% = prep & 33% = upload
// Note: These numbers are very rough.
// The size of each segment in the 100% progress
double segmentSize = buffer.getSize() * 100.0 / totalLocations;
while (success && buffer.getLastLocationRead() < track.getStopId()) {
setProgress(track, totalLocationsRead, segmentSize, 0.0);
progressIndicator.setProgressMessage(
context.getString(R.string.progress_message_reading_track));
providerUtils.getTrackPoints(track, buffer);
if (buffer.getLocationsLoaded() <= 0) {
continue;
}
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
uploadMarker(track, buffer.get(0), true);
}
last = buffer.get(buffer.getLocationsLoaded() - 1);
// Extract the elevation profile.
for (int i = 0; i < buffer.getLocationsLoaded(); i++) {
// Add all points to the smoothing buffer.
Location l = buffer.get(i);
if (l != null && MyTracksUtils.isValidLocation(l)) {
elevationBuffer.setNext(metricUnits ? l.getAltitude()
: l.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(l);
totalDistance += dist;
}
lastLocation = l;
// Only add some data points to keep the url short.
if (i % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
}
setProgress(track, totalLocationsRead, segmentSize, 0.3333);
progressIndicator.setProgressMessage(
context.getString(R.string.progress_message_preparing_track));
ArrayList<Track> splitTracks = prepareLocations(track, buffer);
int i = 0;
for (Track t : splitTracks) {
if (part > 1) {
t.setName(t.getName() + " "
+ String.format(context.getString(R.string.part), part));
}
part++;
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: Prepared feature for upload w/ "
+ t.getLocations().size() + " points.");
//
// Transmit tracks via GData feed:
// -------------------------------
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth);
if (!uploadTrackPoints(track, t,
totalLocationsRead, segmentSize)) {
success = false;
return false;
}
i++;
}
totalLocationsRead += buffer.getLocationsLoaded();
}
return true;
}
/** /**
* Uploads a given list of tracks to Google MyMaps using the maps GData feed. * Uploads a given list of tracks to Google MyMaps using the maps GData feed.
*
* @param track the track meta-data is used for the map name
*/ */
private boolean uploadTrackPoints(final Track track, final Track currentTrack, private boolean uploadTrackPoints(final Track track) {
int totalLocationsRead, double segmentSize) {
setProgress(track, totalLocationsRead, segmentSize, 0.6666);
progressIndicator.setProgressMessage(
context.getString(R.string.progress_message_sending_mymaps));
return wrapper.runQuery(new QueryFunction() { return wrapper.runQuery(new QueryFunction() {
@Override @Override
public void query(MapsClient client) public void query(MapsClient client)
throws AuthenticationException, IOException, Exception { throws AuthenticationException, IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId); String featureFeed = MapsClient.getFeaturesFeed(mapId);
Log.d(MyTracksConstants.TAG, "Feature feed url: " + featureFeed); Log.d(MyTracksConstants.TAG, "Feature feed url: " + featureFeed);
uploadTrackPoints(track, currentTrack, client, featureFeed); uploadTrackPoints(track, client, featureFeed);
} }
}); });
} }
/** private boolean uploadTrackPoints(Track splitTrack,
* Creates a new map for the given track.
*
* @param track The track that will be uploaded to this map
* @return True on success.
*/
private boolean createNewMap(final Track track) {
progressIndicator.setProgressMessage(context
.getString(R.string.progress_message_creating_map));
return wrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
Log.d(MyTracksConstants.TAG, "Creating a new map.");
String mapFeed = MapsClient.getMapsFeed();
Log.d(MyTracksConstants.TAG, "Map feed is " + mapFeed);
MyMapsMapMetadata metaData = new MyMapsMapMetadata();
metaData.setTitle(track.getName());
metaData.setDescription(originalDescription + " - "
+ track.getCategory() + " - "
+ context.getString(R.string.new_map_description));
SharedPreferences preferences = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
boolean mapPublic = true;
if (preferences != null) {
mapPublic = preferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
}
metaData.setSearchable(mapPublic);
Entry entry = MyMapsGDataConverter.getMapEntryForMetadata(metaData);
Log.d(MyTracksConstants.TAG, "Title: " + entry.getTitle());
Entry map = client.createEntry(mapFeed, auth.getAuthToken(), entry);
mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
Log.d(MyTracksConstants.TAG, "New map id is: " + mapId);
}
});
}
private boolean uploadTrackPoints(final Track track,
Track splitTrack,
MapsClient client, MapsClient client,
String featureFeed) String featureFeed)
throws IOException, Exception { throws IOException, Exception {
Entry entry = null; Entry entry = null;
if (splitTrack.getId() != track.getId() int numLocations = splitTrack.getLocations().size();
|| splitTrack.getLocations().size() < 2) { if (numLocations < 2) {
// Need at least two points for a polyline: // Need at least two points for a polyline:
Log.w(MyTracksConstants.TAG, "Not uploading too few points");
return true; return true;
} }
@@ -435,6 +569,29 @@ public class SendToMyMaps {
return true; return true;
} }
/**
* Builds a MyMapsFeature from a track.
*
* @param track the track
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsLineFeature(Track track) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.LINE);
if (track.getName().length() < 1) {
// Features must have a name (otherwise GData upload may fail):
myMapsFeature.setTitle("-");
} else {
myMapsFeature.setTitle(track.getName());
}
myMapsFeature.setColor(0x80FF0000);
for (Location loc : track.getLocations()) {
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
}
return myMapsFeature;
}
/** /**
* Uploads all of the waypoints associated with this track to map. * Uploads all of the waypoints associated with this track to map.
* *
@@ -494,185 +651,6 @@ public class SendToMyMaps {
}); });
} }
public SendToMyMaps(Activity context, String mapId, AuthManager auth,
long trackId, ProgressIndicator progressIndicator) {
this.context = context;
this.mapId = mapId;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
Log.d(MyTracksConstants.TAG, "Sending to MyMap: trackId = " + trackId);
handlerThread = new HandlerThread("SendToMyMaps");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
doUpload();
}
});
}
public void setOnCompletion(Runnable onCompletion) {
this.onCompletion = onCompletion;
}
public String getMapId() {
return mapId;
}
public boolean wasSuccess() {
return success;
}
public String getStatusMessage() {
return statusMessage;
}
private void doUpload() {
statusMessage = context.getString(R.string.error_sending_to_mymap);
success = true;
try {
gdataConverter = new MyMapsGDataConverter();
progressIndicator.setProgressValue(1);
progressIndicator.setProgressMessage(
context.getString(R.string.progress_message_reading_track));
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
originalDescription = track.getDescription();
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
wrapper = new MyMapsGDataWrapper(context);
wrapper.setAuthManager(auth);
wrapper.setRetryOnAuthFailure(true);
distances = new Vector<Double>();
elevations = new Vector<Double>();
elevationBuffer =
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
// Create a new map if necessary:
if (mapId.equals("new")) {
if (!createNewMap(track)) {
success = false;
}
}
// Upload all of the segments of the track.
if (success) {
if (!uploadAllTrackPoints(track)) {
success = false;
}
}
// Start end end markers.
if (success) {
// Put an end marker at the last point of the last valid segment:
if (last != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "</p>");
if (!uploadMarker(track, last, false)) {
success = false;
}
}
}
// Put waypoints.
if (success) {
if (!uploadWaypoints(track)) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: upload waypoints failed.");
success = false;
}
}
if (success) {
if (mapId.equals("new")) {
statusMessage =
context.getString(R.string.status_new_mymap_has_been_created);
} else {
statusMessage =
context.getString(R.string.status_tracks_have_been_uploaded);
}
}
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Done: " + success);
progressIndicator.setProgressValue(100);
} catch (XmlPullParserException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
} finally {
if (wrapper != null) {
wrapper.cleanUp();
}
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.run();
}
}
});
}
}
/**
* Builds a MyMapsFeature from a track.
*
* @param track the track
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsLineFeature(Track track) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.LINE);
if (track.getName().length() < 1) {
// Features must have a name (otherwise GData upload may fail):
myMapsFeature.setTitle("-");
} else {
myMapsFeature.setTitle(track.getName());
}
myMapsFeature.setColor(0x80FF0000);
for (Location loc : track.getLocations()) {
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
}
return myMapsFeature;
}
/**
* Builds a placemark MyMapsFeature from a track.
*
* @param track the track
* @param isStart true if it's the start of the track, or false for end
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsPlacemarkFeature(Context context,
Track track, Location loc, boolean isStart) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.MARKER);
if (isStart) {
myMapsFeature.setIconUrl(START_ICON_URL);
} else {
myMapsFeature.setIconUrl(END_ICON_URL);
}
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
String name = track.getName() + " "
+ (isStart ? context.getString(R.string.start)
: context.getString(R.string.end));
myMapsFeature.setTitle(name);
myMapsFeature.setDescription(isStart ? "" : track.getDescription());
return myMapsFeature;
}
/** /**
* Builds a MyMapsFeature from a track. * Builds a MyMapsFeature from a track.
* *
@@ -695,4 +673,17 @@ public class SendToMyMaps {
myMapsFeature.setDescription(wpt.getDescription().replaceAll("\n", "<br>")); myMapsFeature.setDescription(wpt.getDescription().replaceAll("\n", "<br>"));
return myMapsFeature; return myMapsFeature;
} }
/**
* Sets the current upload progress.
*/
private void updateProgress() {
// The percent of the total that represents the completed part of this
// segment.
int totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
/ (totalLocations * 3);
totalPercentage = Math.min(99, totalPercentage);
progressIndicator.setProgressValue(totalPercentage);
}
} }
@@ -18,7 +18,6 @@ package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackBuffer;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.MyTracksUtils;
@@ -254,55 +253,8 @@ public class TrackWriter {
void writeDocument() { void writeDocument() {
Log.d(MyTracksConstants.TAG, "Started writing track."); Log.d(MyTracksConstants.TAG, "Started writing track.");
writer.writeHeader(); writer.writeHeader();
TrackBuffer buffer = new TrackBuffer(1024); // TODO: Fix ordering (in GPX waypoints should come first)
Location last = null; writeLocations();
boolean wroteFirst = false;
boolean segmentOpen = false;
int nValidLocations = 0;
// Fetch small pieces of the track.
while (buffer.getLastLocationRead() < track.getStopId()) {
Log.d(MyTracksConstants.TAG,
"Reading track points starting at: " + buffer.getLastLocationRead());
providerUtils.fillTrackPoints(track, buffer);
if (!wroteFirst) {
Location first = buffer.findStartLocation();
writer.writeBeginTrack(first);
wroteFirst = true;
}
Log.d(MyTracksConstants.TAG,
"Reading " + buffer.getLocationsLoaded()
+ " Ending at: " + buffer.getLastLocationRead());
for (int i = 0; i < buffer.getLocationsLoaded(); i++) {
Location location = buffer.get(i);
if (MyTracksUtils.isValidLocation(location)) {
nValidLocations++;
if (!segmentOpen) {
writer.writeOpenSegment();
segmentOpen = true;
}
writer.writeLocation(location);
if (nValidLocations >= 2) {
last = location;
}
} else {
nValidLocations = 0;
last = null;
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
}
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(last);
}
writeWaypoints(track.getId()); writeWaypoints(track.getId());
writer.writeFooter(); writer.writeFooter();
writer.close(); writer.close();
@@ -310,4 +262,66 @@ public class TrackWriter {
Log.d(MyTracksConstants.TAG, "Done writing track."); Log.d(MyTracksConstants.TAG, "Done writing track.");
errorMessage = R.string.io_write_finished; errorMessage = R.string.io_write_finished;
} }
private void writeLocations() {
boolean wroteFirst = false;
boolean segmentOpen = false;
Location lastLoc = null, loc = null;
boolean isLastValid = false;
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
return;
}
do {
if (loc == null) loc = new Location("");
providerUtils.fillLocation(locationsCursor, loc);
boolean isValid = MyTracksUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(lastLoc);
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(lastLoc);
}
// Write the current point
writer.writeLocation(loc);
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
// Swap loc and lastLoc (so lastLoc is reused)
Location tmp = lastLoc;
lastLoc = loc;
loc = tmp;
if (loc != null) loc.reset();
isLastValid = isValid;
} while (locationsCursor.moveToNext());
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(lastLoc);
}
}
} }
@@ -102,7 +102,7 @@ public class TrackWriterFactory {
* @param format the output format to write in * @param format the output format to write in
* @return the new track writer * @return the new track writer
*/ */
public static TrackWriter newWriter(Context context, private static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils, MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) { Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context); TrackFormatWriter writer = format.newFormatWriter(context);
@@ -15,7 +15,7 @@
*/ */
package com.google.android.apps.mytracks.io.backup; package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.util.ApiFeatures;
import android.app.backup.BackupManager; import android.app.backup.BackupManager;
import android.content.Context; import android.content.Context;
@@ -66,8 +66,9 @@ public abstract class BackupPreferencesListener
/** /**
* Creates and returns a proper instance of the listener for this device. * Creates and returns a proper instance of the listener for this device.
*/ */
public static BackupPreferencesListener create(Context context) { public static BackupPreferencesListener create(
if (MyTracksConstants.ANDROID_API_LEVEL >= 8) { Context context, ApiFeatures apiFeatures) {
if (apiFeatures.hasBackup()) {
return new BackupPreferencesListenerImpl(context); return new BackupPreferencesListenerImpl(context);
} else { } else {
return new DummyBackupPreferencesListener(); return new DummyBackupPreferencesListener();
@@ -1,62 +0,0 @@
/*
* 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.services;
import android.content.Context;
/**
* This class will periodically announce the user's trip statistics. This class
* is a wrapper for cupcake devices.
*
* @author Sandor Dornbush
*/
public class SafeStatusAnnouncerTask implements PeriodicTask {
private StatusAnnouncerTask announcer;
/* class initialization fails when this throws an exception */
static {
try {
Class.forName(
"com.google.android.apps.mytracks.services.StatusAnnouncerTask");
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
} catch (LinkageError er) {
throw new RuntimeException(er);
}
}
/* calling here forces class initialization */
public static void checkAvailable() {
}
public SafeStatusAnnouncerTask(Context context) {
announcer = new StatusAnnouncerTask(context);
}
public void run(TrackRecordingService service) {
announcer.run(service);
}
public void shutdown() {
announcer.shutdown();
}
@Override
public void start() {
}
}
@@ -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.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.content.Context;
import android.util.Log;
/**
* Factory for producing a proper {@link SignalStrengthTask} according to the
* current API level.
*
* @author Rodrigo Damazio
*/
public class SignalStrengthTaskFactory {
private final boolean hasModernSignalStrength;
SignalStrengthTaskFactory(ApiFeatures apiFeatures) {
this.hasModernSignalStrength = apiFeatures.hasModernSignalStrength();
}
public PeriodicTask create(Context context) {
if (hasModernSignalStrength) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService using modern signal strength api.");
return new SignalStrengthTaskModern(context);
} else {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService using legacy signal strength api.");
return new SignalStrengthTask(context);
}
}
}
@@ -0,0 +1,61 @@
/*
* 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.util.ApiFeatures;
import android.content.Context;
import android.media.AudioManager;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
* an API-level-safe way.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory {
private final boolean hasTts;
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
}
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if announcements are not supported
*/
public PeriodicTask create(Context context) {
if (hasTts) {
return new StatusAnnouncerTask(context);
} else {
return null;
}
}
/**
* Returns the appropriate volume stream for controlling announcement
* volume.
*/
public int getVolumeStream() {
if (hasTts) {
return StatusAnnouncerTask.getVolumeStream();
} else {
return AudioManager.USE_DEFAULT_STREAM_TYPE;
}
}
}
@@ -26,6 +26,7 @@ import com.google.android.maps.mytracks.R;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.util.Log; import android.util.Log;
import java.util.Locale; import java.util.Locale;
@@ -35,8 +36,7 @@ import java.util.Locale;
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class StatusAnnouncerTask public class StatusAnnouncerTask implements PeriodicTask {
implements TextToSpeech.OnInitListener, PeriodicTask {
/** /**
* The rate at which announcements are spoken. * The rate at which announcements are spoken.
@@ -56,12 +56,12 @@ public class StatusAnnouncerTask
/** /**
* The interface to the text to speech engine. * The interface to the text to speech engine.
*/ */
private TextToSpeech tts = null; private final TextToSpeech tts;
/** /**
* The response recieved from the TTS engine ater initialization. * The response recieved from the TTS engine ater initialization.
*/ */
private int ttsStatus = TextToSpeech.ERROR; private boolean ready = false;
/** /**
* Constructs the announcer and start the TTS engine. * Constructs the announcer and start the TTS engine.
@@ -70,19 +70,40 @@ public class StatusAnnouncerTask
this.context = context; this.context = context;
this.stringUtils = new StringUtils(context); this.stringUtils = new StringUtils(context);
tts = new TextToSpeech(context, this); // We can't have this class also be the listener, otherwise it's unsafe to
// Force the language to be the same as the string we will be speaking. // reference it in Cupcake (even if we don't instantiate it).
tts.setLanguage(Locale.getDefault()); tts = new TextToSpeech(context, new OnInitListener() {
// Slow down the speed just a bit as it is hard to hear when exercising. @Override
tts.setSpeechRate(TTS_SPEECH_RATE); public void onInit(int status) {
onTtsInit(status);
}
});
} }
/** /**
* Notifies that the tts engine is done with initialization. * Called when the TTS engine is initialized.
*/ */
public void onInit(int status) { private void onTtsInit(int status) {
this.ttsStatus = status;
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status); Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
this.ready = (status == TextToSpeech.SUCCESS);
if (ready) {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported
// TODO: Somehow use announcement strings from English too
Log.w(MyTracksConstants.TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
} }
/** /**
@@ -90,8 +111,8 @@ public class StatusAnnouncerTask
*/ */
@Override @Override
public void run(TrackRecordingService service) { public void run(TrackRecordingService service) {
if (ttsStatus != TextToSpeech.SUCCESS && tts != null) { if (!ready) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not initialized."); Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready.");
return; return;
} }
@@ -179,4 +200,8 @@ public class StatusAnnouncerTask
@Override @Override
public void start() { public void start() {
} }
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
} }
@@ -27,6 +27,7 @@ import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -106,7 +107,7 @@ public class TrackRecordingService extends Service implements LocationListener {
/** /**
* Status announcer executer. * Status announcer executer.
*/ */
private PeriodicTaskExecuter executer; private PeriodicTaskExecuter announcementExecuter;
private TaskExecuterManager signalManager; private TaskExecuterManager signalManager;
private SplitManager splitManager; private SplitManager splitManager;
@@ -181,18 +182,6 @@ public class TrackRecordingService extends Service implements LocationListener {
*/ */
private int announcementFrequency = -1; private int announcementFrequency = -1;
private static boolean mTTSAvailable;
/* establish whether the "new" class is available to us */
static {
try {
SafeStatusAnnouncerTask.checkAvailable();
mTTSAvailable = true;
} catch (Throwable t) {
mTTSAvailable = false;
}
}
/* /*
* Utility functions * Utility functions
*/ */
@@ -645,17 +634,12 @@ public class TrackRecordingService extends Service implements LocationListener {
(NotificationManager) getSystemService(NOTIFICATION_SERVICE); (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
splitManager = new SplitManager(this); splitManager = new SplitManager(this);
try {
signalManager = SignalStrengthTaskFactory strengthTaskFactory =
new TaskExecuterManager(-1, new SignalStrengthTaskModern(this), this); new SignalStrengthTaskFactory(ApiFeatures.getInstance());
Log.d(MyTracksConstants.TAG, signalManager =
"TrackRecordingService using modern signal strength api."); new TaskExecuterManager(-1, strengthTaskFactory.create(this), this);
} catch (LinkageError e) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService could not load modern signal strength.", e);
signalManager =
new TaskExecuterManager(-1, new SignalStrengthTask(this), this);
}
prefManager = new PreferenceManager(this); prefManager = new PreferenceManager(this);
prefManager.onSharedPreferenceChanged(null); prefManager.onSharedPreferenceChanged(null);
registerLocationListener(); registerLocationListener();
@@ -682,12 +666,16 @@ public class TrackRecordingService extends Service implements LocationListener {
* the announcements, otherwise this method is no-op. * the announcements, otherwise this method is no-op.
*/ */
private void setUpAnnouncer() { private void setUpAnnouncer() {
if (mTTSAvailable && announcementFrequency != -1) { if (announcementFrequency != -1) {
if (executer == null) { if (announcementExecuter == null) {
SafeStatusAnnouncerTask announcer = new SafeStatusAnnouncerTask(this); StatusAnnouncerFactory statusAnnouncerFactory =
executer = new PeriodicTaskExecuter(announcer, this); new StatusAnnouncerFactory(ApiFeatures.getInstance());
PeriodicTask announcer = statusAnnouncerFactory.create(this);
if (announcer == null) return;
announcementExecuter = new PeriodicTaskExecuter(announcer, this);
} }
executer.scheduleTask(announcementFrequency * 60000); announcementExecuter.scheduleTask(announcementFrequency * 60000);
} }
} }
@@ -702,8 +690,8 @@ public class TrackRecordingService extends Service implements LocationListener {
isRecording = false; isRecording = false;
showNotification(); showNotification();
unregisterLocationListener(); unregisterLocationListener();
if (executer != null) { if (announcementExecuter != null) {
executer.shutdown(); announcementExecuter.shutdown();
} }
splitManager.shutdown(); splitManager.shutdown();
super.onDestroy(); super.onDestroy();
@@ -1014,15 +1002,13 @@ public class TrackRecordingService extends Service implements LocationListener {
public void setAnnouncementFrequency(int announcementFrequency) { public void setAnnouncementFrequency(int announcementFrequency) {
this.announcementFrequency = announcementFrequency; this.announcementFrequency = announcementFrequency;
if (mTTSAvailable) { if (announcementFrequency == -1) {
if (announcementFrequency == -1) { if (announcementExecuter != null) {
if (executer != null) { announcementExecuter.shutdown();
executer.shutdown(); announcementExecuter = null;
executer = null;
}
} else {
setUpAnnouncer();
} }
} else {
setUpAnnouncer();
} }
} }
@@ -0,0 +1,89 @@
/*
* 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.os.Build;
/**
* 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;
/**
* 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() {}
/**
* 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;
}
}
@@ -37,7 +37,7 @@ public class FileUtils {
* A set of characters that are prohibited from being in file names. * A set of characters that are prohibited from being in file names.
*/ */
private static final Pattern PROHIBITED_CHAR_PATTERN = private static final Pattern PROHIBITED_CHAR_PATTERN =
Pattern.compile("[^ A-Za-z0-9_.()]+"); Pattern.compile("[^ A-Za-z0-9_.()-]+");
/** /**
* Builds a path inside the My Tracks directory in the SD card. * Builds a path inside the My Tracks directory in the SD card.
@@ -69,8 +69,6 @@ public class FileUtils {
* Normalizes the input string and make sure it is a valid fat32 file name. * Normalizes the input string and make sure it is a valid fat32 file name.
* *
* @param name the name to normalize * @param name the name to normalize
* @param overheadSize the number of additional characters that will be added
* to the name after sanitization
* @return the sanitized name * @return the sanitized name
*/ */
String sanitizeName(String name) { String sanitizeName(String name) {
@@ -0,0 +1,243 @@
/*
* 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;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Projection;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.location.Location;
import android.test.AndroidTestCase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* Tests for the MyTracks map overlay.
*
* @author Bartlomiej Niechwiej
*/
public class MyTracksOverlayTest extends AndroidTestCase {
private Canvas canvas;
private MockMyTracksOverlay myTracksOverlay;
private MapView mockView;
private Projection mockProjection;
/**
* A mock version of {@code MyTracksOverlay} that does not use
* {@class MapView}.
*/
private class MockMyTracksOverlay extends MyTracksOverlay {
public MockMyTracksOverlay(Context context) {
super(context);
}
@Override
Projection getMapProjection(MapView mapView) {
return mockProjection;
}
@Override
Rect getMapViewRect(MapView mapView) {
return new Rect(0, 0, 100, 100);
}
@Override
Path newPath() {
return new MockPath();
}
}
/**
* A mock class that intercepts {@code Path}'s and records calls to
* {@code #moveTo()} and {@code #lineTo()}.
*/
private static class MockPath extends Path {
/** A list of disjoined path segments. */
public final List<List<PointF>> segments = new LinkedList<List<PointF>>();
/** The total number of points in this path. */
public int totalPoints;
private List<PointF> currentSegment;
@Override
public void lineTo(float x, float y) {
super.lineTo(x, y);
assertNotNull(currentSegment);
currentSegment.add(new PointF(x, y));
totalPoints++;
}
@Override
public void moveTo(float x, float y) {
super.moveTo(x, y);
segments.add(currentSegment =
new ArrayList<PointF>(Arrays.asList(new PointF(x, y))));
totalPoints++;
}
}
/**
* A mock {@code Projection} that acts as the identity matrix.
*/
private static class MockProjection implements Projection {
@Override
public Point toPixels(GeoPoint in, Point out) {
return out;
}
@Override
public float metersToEquatorPixels(float meters) {
return meters;
}
@Override
public GeoPoint fromPixels(int x, int y) {
return new GeoPoint(y, x);
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
canvas = new Canvas();
myTracksOverlay = new MockMyTracksOverlay(getContext());
// Enable drawing.
myTracksOverlay.setTrackDrawingEnabled(true);
mockView = null;
mockProjection = new MockProjection();
}
public void testAddLocation() throws Exception {
Location location = new Location("gps");
location.setLatitude(10);
location.setLongitude(20);
myTracksOverlay.addLocation(location);
assertEquals(1, myTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints());
location.setLatitude(20);
location.setLongitude(30);
myTracksOverlay.addLocation(location);
assertEquals(2, myTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints());
assertNull(myTracksOverlay.getLastPath());
// Draw and make sure that we don't lose any point.
myTracksOverlay.draw(canvas, mockView, false);
assertEquals(2, myTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints());
assertNotNull(myTracksOverlay.getLastPath());
assertEquals(2, ((MockPath)myTracksOverlay.getLastPath()).totalPoints);
myTracksOverlay.draw(canvas, mockView, true);
assertEquals(2, myTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints());
assertNotNull(myTracksOverlay.getLastPath());
}
public void testClearPoints() throws Exception {
Location location = new Location("gps");
location.setLatitude(10);
location.setLongitude(20);
myTracksOverlay.addLocation(location);
assertEquals(1, myTracksOverlay.getNumLocations());
myTracksOverlay.clearPoints();
assertEquals(0, myTracksOverlay.getNumLocations());
// Same after drawing on canvas.
final int locations = 100;
for (int i = 0; i < locations; ++i) {
myTracksOverlay.addLocation(location);
}
assertEquals(locations, myTracksOverlay.getNumLocations());
myTracksOverlay.draw(canvas, mockView, false);
myTracksOverlay.draw(canvas, mockView, true);
myTracksOverlay.clearPoints();
assertEquals(0, myTracksOverlay.getNumLocations());
}
public void testAddWaypoint() throws Exception {
Location location = new Location("gps");
location.setLatitude(10);
location.setLongitude(20);
Waypoint waypoint = new Waypoint();
waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint);
assertEquals(1, myTracksOverlay.getNumWaypoints());
assertEquals(0, myTracksOverlay.getNumLocations());
assertNull(myTracksOverlay.getLastPath());
final int waypoints = 10;
for (int i = 0; i < waypoints; ++i) {
waypoint = new Waypoint();
waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint);
}
assertEquals(1 + waypoints, myTracksOverlay.getNumWaypoints());
assertEquals(0, myTracksOverlay.getNumLocations());
assertNull(myTracksOverlay.getLastPath());
}
public void testClearWaypoints() throws Exception {
Location location = new Location("gps");
location.setLatitude(10);
location.setLongitude(20);
Waypoint waypoint = new Waypoint();
waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint);
assertEquals(1, myTracksOverlay.getNumWaypoints());
myTracksOverlay.clearWaypoints();
assertEquals(0, myTracksOverlay.getNumWaypoints());
}
public void testDrawing() {
Location location = new Location("gps");
location.setLatitude(10);
for (int i = 0; i < 40; ++i) {
location.setLongitude(20 + i);
Waypoint waypoint = new Waypoint();
waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint);
}
for (int i = 0; i < 100; ++i) {
location = new Location("gps");
location.setLatitude(20 + i / 2);
location.setLongitude(150 - i);
myTracksOverlay.addLocation(location);
}
// Shadow.
myTracksOverlay.draw(canvas, mockView, true);
// We don't expect to do anything if
assertNull(myTracksOverlay.getLastPath());
assertEquals(40, myTracksOverlay.getNumWaypoints());
assertEquals(100, myTracksOverlay.getNumLocations());
// No shadow.
myTracksOverlay.draw(canvas, mockView, false);
assertNotNull(myTracksOverlay.getLastPath());
assertTrue(myTracksOverlay.getLastPath() instanceof MockPath);
MockPath path = (MockPath) myTracksOverlay.getLastPath();
assertEquals(40, myTracksOverlay.getNumWaypoints());
assertEquals(100, myTracksOverlay.getNumLocations());
assertEquals(100, path.totalPoints);
// TODO: Check the points from the path (and the segments).
}
}
@@ -2,21 +2,23 @@
package com.google.android.apps.mytracks.io; package com.google.android.apps.mytracks.io;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA; import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.leq;
import static org.easymock.EasyMock.same; import static org.easymock.EasyMock.same;
import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackBuffer;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.MatrixCursor;
import android.location.Location; import android.location.Location;
import android.provider.BaseColumns;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -25,6 +27,7 @@ import java.io.OutputStream;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import org.easymock.IAnswer; import org.easymock.IAnswer;
import org.easymock.IArgumentMatcher;
import org.easymock.IMocksControl; import org.easymock.IMocksControl;
/** /**
@@ -226,6 +229,8 @@ public class TrackWriterTest extends AndroidTestCase {
expect(providerUtils.getWaypointsCursor( expect(providerUtils.getWaypointsCursor(
TRACK_ID, 0, MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS)) TRACK_ID, 0, MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS))
.andStubReturn(null); .andStubReturn(null);
expect(providerUtils.getLocationsCursor(
eq(TRACK_ID), leq(0L), leq(0), eq(false))).andStubReturn(null);
// Set expected mock behavior // Set expected mock behavior
formatWriter.writeHeader(); formatWriter.writeHeader();
@@ -242,61 +247,78 @@ public class TrackWriterTest extends AndroidTestCase {
public void testWriteDocument() { public void testWriteDocument() {
writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); writer = new TrackWriter(getContext(), providerUtils, track, formatWriter);
Location l1 = new Location("fake1"); final Location[] locs = {
Location l2 = new Location("fake2"); new Location("fake0"),
Location l3 = new Location("fake3"); new Location("fake1"),
Location l4 = new Location("fake4"); new Location("fake2"),
Location l5 = new Location("fake5"); new Location("fake3"),
Location l6 = new Location("fake6"); new Location("fake4"),
Waypoint p1 = new Waypoint(); new Location("fake5"),
Waypoint p2 = new Waypoint(); };
Waypoint[] wps = { new Waypoint(), new Waypoint(), new Waypoint() };
addLocations(l1, l2, l3, l4, l5, l6); // Fill locations with valid values
stubBufferFill( fillLocations(locs);
new Location[] { l1, l2, l3, l4 },
new Location[] { l5, l6 });
track.setStopId(6L);
// Make location 3 invalid // Make location 3 invalid
l3.setLatitude(100); locs[2].setLatitude(100);
// Set up cursors
// We use fake columns since the cursor is only read by the provider utils
final MatrixCursor locCursor =
new MatrixCursor(new String[] { BaseColumns._ID }, 6);
for (int i = 1; i <= 6; i++) {
locCursor.newRow().add(i);
}
expect(providerUtils.getLocationsCursor(
eq(TRACK_ID), leq(0L), leq(0), eq(false))).andStubReturn(locCursor);
providerUtils.fillLocation(same(locCursor), isA(Location.class));
EasyMock.expectLastCall().andStubAnswer(new IAnswer<Void>() {
@Override
public Void answer() throws Throwable {
Location loc = (Location) EasyMock.getCurrentArguments()[1];
loc.set(locs[locCursor.getPosition()]);
return null;
}
});
MatrixCursor wpCursor =
new MatrixCursor(new String[] { BaseColumns._ID }, 3);
wpCursor.newRow().add(1);
wpCursor.newRow().add(2);
wpCursor.newRow().add(3);
expect(providerUtils.getWaypointsCursor(
eq(TRACK_ID), leq(0L),
eq(MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS)))
.andStubReturn(wpCursor);
expect(providerUtils.createWaypoint(wpCursor))
.andStubAnswer(stubCursorToArray(wpCursor, wps));
// Begin the track // Begin the track
formatWriter.writeHeader(); formatWriter.writeHeader();
formatWriter.writeBeginTrack(l1); formatWriter.writeBeginTrack(locEq(locs[0]));
// Write locations 1-2 // Write locations 1-2
formatWriter.writeOpenSegment(); formatWriter.writeOpenSegment();
formatWriter.writeLocation(l1); formatWriter.writeLocation(locEq(locs[0]));
formatWriter.writeLocation(l2); formatWriter.writeLocation(locEq(locs[1]));
formatWriter.writeCloseSegment(); formatWriter.writeCloseSegment();
// Location 3 is not written - it's invalid // Location 3 is not written - it's invalid
// Write locations 4-6 // Write locations 4-6
formatWriter.writeOpenSegment(); formatWriter.writeOpenSegment();
formatWriter.writeLocation(l4); formatWriter.writeLocation(locEq(locs[3]));
formatWriter.writeLocation(l5); formatWriter.writeLocation(locEq(locs[4]));
formatWriter.writeLocation(l6); formatWriter.writeLocation(locEq(locs[5]));
formatWriter.writeCloseSegment(); formatWriter.writeCloseSegment();
// End the track // End the track
formatWriter.writeEndTrack(l6); formatWriter.writeEndTrack(locEq(locs[5]));
// Expect reading/writing of the waypoints // Expect reading/writing of the waypoints (except the first)
Cursor cursor = mocksControl.createMock(Cursor.class); formatWriter.writeWaypoint(wps[1]);
expect(providerUtils.getWaypointsCursor( formatWriter.writeWaypoint(wps[2]);
TRACK_ID, 0, MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS))
.andStubReturn(cursor);
expect(cursor.moveToFirst()).andReturn(true);
expect(cursor.moveToNext()).andReturn(true);
expect(providerUtils.createWaypoint(cursor)).andReturn(p1);
formatWriter.writeWaypoint(p1);
expect(cursor.moveToNext()).andReturn(true);
expect(providerUtils.createWaypoint(cursor)).andReturn(p2);
formatWriter.writeWaypoint(p2);
expect(cursor.moveToNext()).andReturn(false).anyTimes();
cursor.close();
formatWriter.writeFooter(); formatWriter.writeFooter();
formatWriter.close(); formatWriter.close();
@@ -308,7 +330,47 @@ public class TrackWriterTest extends AndroidTestCase {
mocksControl.verify(); mocksControl.verify();
} }
private void addLocations(Location... locs) { private static Location locEq(final Location loc) {
EasyMock.reportMatcher(new IArgumentMatcher() {
@Override
public boolean matches(Object locObj2) {
if (locObj2 == null || loc == null) return loc == locObj2;
Location loc2 = (Location) locObj2;
return loc.hasAccuracy() == loc2.hasAccuracy()
&& (!loc.hasAccuracy() || loc.getAccuracy() == loc2.getAccuracy())
&& loc.hasAltitude() == loc2.hasAltitude()
&& (!loc.hasAltitude() || loc.getAltitude() == loc2.getAltitude())
&& loc.hasBearing() == loc2.hasBearing()
&& (!loc.hasBearing() || loc.getBearing() == loc2.getBearing())
&& loc.hasSpeed() == loc2.hasSpeed()
&& (!loc.hasSpeed() || loc.getSpeed() == loc2.getSpeed())
&& loc.getLatitude() == loc2.getLatitude()
&& loc.getLongitude() == loc2.getLongitude()
&& loc.getTime() == loc2.getTime();
}
@Override
public void appendTo(StringBuffer buffer) {
buffer.append("locEq(");
buffer.append(loc);
buffer.append(")");
}
});
return null;
}
private <T> IAnswer<T> stubCursorToArray(
final MatrixCursor cursor, final T[] values) {
return new IAnswer<T>() {
@Override
public T answer() throws Throwable {
return values[cursor.getPosition()];
}
};
}
private void fillLocations(Location... locs) {
assertTrue(locs.length < 90); assertTrue(locs.length < 90);
for (int i = 0; i < locs.length; i++) { for (int i = 0; i < locs.length; i++) {
Location location = locs[i]; Location location = locs[i];
@@ -316,46 +378,4 @@ public class TrackWriterTest extends AndroidTestCase {
location.setLongitude(i + 1); location.setLongitude(i + 1);
} }
} }
/**
* Defines the behaviour of filling the track buffer when a read is
* requested.
* The IDs of the locations will be their sequential number.
*
* @param feeds is a list of location arrays, each element of which
* will be fed into the track buffer on each call
*/
private void stubBufferFill(final Location[]... feeds) {
providerUtils.fillTrackPoints(same(track), isA(TrackBuffer.class));
EasyMock.expectLastCall().andStubAnswer(new IAnswer<Void>() {
private int lastId = 1;
private int reads = 0;
@Override
public Void answer() throws Throwable {
// Get the buffer from the arguments
Object[] args = EasyMock.getCurrentArguments();
assertEquals(2, args.length);
TrackBuffer buffer = (TrackBuffer) args[1];
assertNotNull(buffer);
// Check that we still have data to feed to the buffer
if (reads >= feeds.length) {
fail("More buffer reads than expected");
}
// Fill the buffer
buffer.reset();
Location[] locations = feeds[reads];
for (int i = 0; i < locations.length; i++) {
buffer.add(locations[i], lastId + i);
}
// Update internal state
lastId += locations.length;
reads++;
return null;
}
});
}
} }
@@ -0,0 +1,72 @@
/*
* 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.util.ApiFeatures;
import android.test.AndroidTestCase;
/**
* Tests for {@link SignalStrengthTaskFactoryTest}.
* These tests require Eclair+ (API level 7) to run.
*
* @author Rodrigo Damazio
*/
public class SignalStrengthTaskFactoryTest extends AndroidTestCase {
/**
* Mock version of the {@link ApiFeatures} class.
*/
private static class MockApiFeatures extends ApiFeatures {
private boolean hasModernStrength;
public void setHasModernSignalStrength(boolean hasModernStrength) {
this.hasModernStrength = hasModernStrength;
}
@Override
public boolean hasModernSignalStrength() {
return hasModernStrength;
}
}
private MockApiFeatures apiFeatures;
@Override
protected void setUp() throws Exception {
super.setUp();
apiFeatures = new MockApiFeatures();
}
public void testCreate() {
apiFeatures.setHasModernSignalStrength(true);
SignalStrengthTaskFactory factory =
new SignalStrengthTaskFactory(apiFeatures);
PeriodicTask task = factory.create(getContext());
assertTrue(task.getClass().getName(),
task instanceof SignalStrengthTaskModern);
}
public void testCreate_legacy() {
apiFeatures.setHasModernSignalStrength(false);
SignalStrengthTaskFactory factory =
new SignalStrengthTaskFactory(apiFeatures);
PeriodicTask task = factory.create(getContext());
assertTrue(task.getClass().getName(), task instanceof SignalStrengthTask);
}
}
@@ -0,0 +1,89 @@
/*
* 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.util.ApiFeatures;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.test.AndroidTestCase;
/**
* Tests for {@link StatusAnnouncerFactory}.
* These tests require Donut+ to run.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactoryTest extends AndroidTestCase {
/**
* Mock version of the {@link ApiFeatures} class.
*/
private class MockApiFeatures extends ApiFeatures {
private boolean hasTts;
public void setHasTextToSpeech(boolean hasTts) {
this.hasTts = hasTts;
}
@Override
public boolean hasTextToSpeech() {
return hasTts;
}
}
private MockApiFeatures apiFeatures;
@Override
protected void setUp() throws Exception {
super.setUp();
apiFeatures = new MockApiFeatures();
}
public void testCreate() {
apiFeatures.setHasTextToSpeech(true);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTask task = factory.create(getContext());
assertTrue(task instanceof StatusAnnouncerTask);
}
public void testCreate_notAvailable() {
apiFeatures.setHasTextToSpeech(false);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTask task = factory.create(getContext());
assertNull(task);
}
public void testGetVolumeStream() {
apiFeatures.setHasTextToSpeech(true);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
assertEquals(
TextToSpeech.Engine.DEFAULT_STREAM,
factory.getVolumeStream());
}
public void testGetVolumeStream_notAvailable() {
apiFeatures.setHasTextToSpeech(false);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
assertEquals(
AudioManager.USE_DEFAULT_STREAM_TYPE,
factory.getVolumeStream());
}
}
@@ -0,0 +1,78 @@
/*
* 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 junit.framework.TestCase;
/**
* Tests the API feature detection code in {@link ApiFeatures}.
* This test requires Froyo+ to run.
*
* @author Rodrigo Damazio
*/
public class ApiFeaturesTest extends TestCase {
private TestableApiFeatures features;
private class TestableApiFeatures extends ApiFeatures {
private int apiLevel;
public void setApiLevel(int apiLevel) {
this.apiLevel = apiLevel;
}
@Override
protected int getApiLevel() {
return apiLevel;
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
features = new TestableApiFeatures();
}
public void testHasBackup() {
for (int i = 3; i <= 7; i++) {
features.setApiLevel(i);
assertFalse(features.hasBackup());
}
features.setApiLevel(8);
assertTrue(features.hasBackup());
}
public void testHasTextToSpeech() {
features.setApiLevel(3);
assertFalse(features.hasTextToSpeech());
for (int i = 4; i <= 8; i++) {
features.setApiLevel(i);
assertTrue(features.hasTextToSpeech());
}
}
public void testHasModernSignalStrength() {
for (int i = 3; i <= 6; i++) {
features.setApiLevel(i);
assertFalse(features.hasModernSignalStrength());
}
features.setApiLevel(7);
assertTrue(features.hasModernSignalStrength());
features.setApiLevel(8);
assertTrue(features.hasModernSignalStrength());
}
}
@@ -31,8 +31,8 @@ import junit.framework.TestCase;
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class FileUtilsTest extends TestCase { public class FileUtilsTest extends TestCase {
private static final String ORIGINAL_NAME = "Swim\10ming ^across: the/ pacific (ocean)."; private static final String ORIGINAL_NAME = "Swim\10ming-^across: the/ pacific (ocean).";
private static final String SANITIZED_NAME = "Swimming across the pacific (ocean)."; private static final String SANITIZED_NAME = "Swimming-across the pacific (ocean).";
private FileUtils fileUtils; private FileUtils fileUtils;
private Set<String> existingFiles; private Set<String> existingFiles;