diff --git a/MyTracks/.classpath b/MyTracks/.classpath index 39a225fb5..7c5cb8f1e 100644 --- a/MyTracks/.classpath +++ b/MyTracks/.classpath @@ -1,10 +1,10 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index 66c4ae207..64afb48f1 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -38,7 +38,6 @@ limitations under the License. shareUrlOnly signalSamplingFrequency splitFrequency - debugDrawBounds autoResumeTrackCurrentRetry diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 3233bd070..386240b6a 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -133,13 +133,4 @@ limitations under the License. android:persistent="false" /> - - - diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index ff43a2647..62117a82c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -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.Waypoint; 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.TripStatisticsBuilder; +import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; @@ -35,7 +37,6 @@ import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; -import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -243,7 +244,9 @@ public class ChartActivity extends Activity implements providerUtils = MyTracksProviderUtils.Factory.get(this); // 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); setContentView(R.layout.mytracks_elevation); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 05f61e1b2..449fe0f2a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -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.SendToDocs; 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.TrackWriterFactory; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; 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.maps.mytracks.R; @@ -53,17 +56,18 @@ import android.location.LocationManager; import android.net.Uri; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; -import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.Window; import android.view.View.OnTouchListener; import android.view.ViewGroup.LayoutParams; +import android.view.Window; import android.view.WindowManager.BadTokenException; import android.widget.RelativeLayout; import android.widget.TabHost; @@ -258,7 +262,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, menuManager = new MenuManager(this); // 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: requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -520,8 +526,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, if (results.hasExtra("mapid")) { sendToMyMapsMapId = results.getStringExtra("mapid"); } - setProgressMessage(getString( - R.string.progress_message_authenticating_mymaps)); + setProgressMessage( + R.string.progress_message_authenticating_mymaps); authenticate(results, MyTracksConstants.SEND_TO_GOOGLE, MyMapsConstants.MAPSHOP_SERVICE); } else { @@ -587,7 +593,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, if (resultCode == RESULT_OK) { setProgressValue(0); setProgressMessage( - getString(R.string.progress_message_authenticating_docs)); + R.string.progress_message_authenticating_docs); authenticate(results, MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely"); } else { @@ -599,7 +605,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, if (resultCode == RESULT_OK) { setProgressValue(30); setProgressMessage( - getString(R.string.progress_message_authenticating_docs)); + R.string.progress_message_authenticating_docs); authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise"); } else { dismissDialogSafely(DIALOG_PROGRESS); @@ -610,7 +616,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, if (results != null && resultCode == RESULT_OK) { Log.d(MyTracksConstants.TAG, "Sending to Docs...."); 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 SendToDocs sender = new SendToDocs(this, authMap.get("wise"), authMap.get("writely"), trackId); @@ -641,30 +647,30 @@ public class MyTracks extends TabActivity implements OnTouchListener, } case MyTracksConstants.SEND_TO_GOOGLE: { if (results != null && resultCode == RESULT_OK) { - final String mapid; + final String mapId; final long trackId; if (results.hasExtra("mapid")) { - mapid = results.getStringExtra("mapid"); + mapId = results.getStringExtra("mapid"); } else { - mapid = "new"; + mapId = "new"; } if (results.hasExtra("trackid")) { trackId = results.getLongExtra("trackid", -1); } else { trackId = selectedTrackId; } - final SendToMyMaps sender = new SendToMyMaps(this, mapid, auth, - trackId, this/*progressIndicator*/); - Runnable onCompletion = new Runnable() { - public void run() { - sendToMyMapsMessage = sender.getStatusMessage(); - sendToMyMapsSuccess = sender.wasSuccess(); + + OnSendCompletedListener onCompletion = new OnSendCompletedListener() { + @Override + public void onSendCompleted(String mapId, boolean success, int statusMessage) { + sendToMyMapsMessage = getString(statusMessage); + sendToMyMapsSuccess = success; if (sendToMyMapsSuccess) { - sendToMyMapsMapId = sender.getMapId(); + sendToMyMapsMapId = mapId; // Update the map id for this track: try { Track track = providerUtils.getTrack(trackId); - track.setMapId(sender.getMapId()); + track.setMapId(mapId); providerUtils.updateTrack(track); } catch (RuntimeException e) { // 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); - sender.run(); + final SendToMyMaps sender = new SendToMyMaps(this, mapId, auth, + trackId, this/*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToMyMaps"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); } else { 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() { public void run() { synchronized (this) { if (progressDialog != null) { - progressDialog.setMessage(text); + progressDialog.setMessage(message); } } } }); } + @Override + public void clearProgressMessage() { + setProgressMessage(""); + } + + @Override public void setProgressValue(final int percent) { runOnUiThread(new Runnable() { public void run() { @@ -1275,7 +1299,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, return; } setProgressValue(0); - setProgressMessage(""); + clearProgressMessage(); showDialogSafely(DIALOG_PROGRESS); if (sendToGoogleDialog.getSendToMyMaps()) { if (!sendToGoogleDialog.getCreateNewMap()) { @@ -1283,8 +1307,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, startActivityForResult(listIntent, MyTracksConstants.GET_MAP); } else { setProgressValue(0); - setProgressMessage(getString( - R.string.progress_message_authenticating_mymaps)); + setProgressMessage( + R.string.progress_message_authenticating_mymaps); authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE, MyMapsConstants.MAPSHOP_SERVICE); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java index a30684cf3..80726e5fc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java @@ -15,8 +15,6 @@ */ package com.google.android.apps.mytracks; -import android.os.Build; - /** * Constants used by the MyTracks application. * @@ -40,11 +38,6 @@ public abstract class MyTracksConstants { */ 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: */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index 0aba9c5fd..32212e145 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -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.Waypoint; 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.util.ApiFeatures; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapActivity; @@ -47,17 +49,16 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.provider.Settings; -import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.SubMenu; import android.view.View; -import android.view.View.OnCreateContextMenuListener; import android.view.Window; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -139,14 +140,16 @@ public class MyTracksMap extends MapActivity */ private HandlerThread updateTrackThread; - /** Handler for updateTrackThread */ + /** + * Handler for updateTrackThread. + */ private Handler updateTrackHandler; private MyTracksProviderUtils providerUtils; /** * A runnable that updates the track from the provider (looking for points - * added after "lastSeenLocationId". + * added after "lastSeenLocationId"). */ private final Runnable updateTrackRunnable = new Runnable() { @Override @@ -154,6 +157,7 @@ public class MyTracksMap extends MapActivity if (!isATrackSelected()) { return; } + readAllNewTrackPoints(); } }; @@ -167,6 +171,7 @@ public class MyTracksMap extends MapActivity if (!isATrackSelected()) { return; } + mapOverlay.clearPoints(); firstSeenLocationId = -1; lastSeenLocationId = -1; @@ -192,13 +197,10 @@ public class MyTracksMap extends MapActivity cursor = providerUtils.getWaypointsCursor( selectedTrackId, 0, MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS); - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - Waypoint wpt = providerUtils.createWaypoint(cursor); - mapOverlay.addWaypoint(wpt); - } while (cursor.moveToNext()); - } + if (cursor != null && cursor.moveToFirst()) { + do { + mapOverlay.addWaypoint(providerUtils.createWaypoint(cursor)); + } while (cursor.moveToNext()); } } catch (RuntimeException 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 - * selected track changes. It will post to the ui thread to update the screen - * elements and move the map to show the selected track. + * A runnable intended to be posted to the {@code #updateTrackThread} after + * the selected track changes. It will post to the UI thread to update + * the screen elements and move the map to show the selected track. */ private final Runnable setSelectedTrackRunnable = new Runnable() { @Override @@ -282,7 +284,9 @@ public class MyTracksMap extends MapActivity super.onCreate(bundle); // 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); @@ -372,12 +376,20 @@ public class MyTracksMap extends MapActivity setSelectedTrack(selectedTrackId); } updateOptionsButton(); - mapOverlay.setDrawBounds(preferences.getBoolean( - getString(R.string.debug_draw_bounds_key), false)); 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. */ @@ -893,8 +905,7 @@ public class MyTracksMap extends MapActivity @Override public void onSharedPreferenceChanged( - final SharedPreferences sharedPreferences, - final String key) { + final SharedPreferences sharedPreferences, final String key) { if (key != null) { uiHandler.post(new Runnable() { @Override @@ -918,10 +929,6 @@ public class MyTracksMap extends MapActivity mapOverlay.setShowEndMarker(!isRecordingSelected()); 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) { if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) { // Recalculate the variation if there was a jump in location > 1km: - if (currentLocation == null - || location.distanceTo(currentLocation) > 1000) { + if (currentLocation == null || + location.distanceTo(currentLocation) > 1000) { setVariation(location); } currentLocation = location; @@ -1039,8 +1046,8 @@ public class MyTracksMap extends MapActivity // Now we already have at least one point, calculate the sampling // frequency long numTotalPoints = lastStoredLocationId - firstSeenLocationId; - samplingFrequency = (int) (1 + - numTotalPoints / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); + samplingFrequency = (int) (1 + numTotalPoints + / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); } providerUtils.fillLocation(cursor, location); @@ -1052,8 +1059,8 @@ public class MyTracksMap extends MapActivity if (numPoints % samplingFrequency == 0 || (!isRecordingSelected() && locationId == lastStoredLocationId) || !MyTracksUtils.isValidLocation(location)) { - // Only allocate a new location if it is going to be kept around. - mapOverlay.addLocation(new Location(location)); + // No need to allocate a new location (we can safely reuse the existing). + mapOverlay.addLocation(location); } numPoints++; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java index 47faf8fad..95abbc93b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java @@ -20,20 +20,24 @@ import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapView; import com.google.android.maps.Overlay; +import com.google.android.maps.Projection; import com.google.android.maps.mytracks.R; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.Region; import android.graphics.drawable.Drawable; import android.location.Location; 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 @@ -43,7 +47,7 @@ import java.util.ArrayList; */ public class MyTracksOverlay extends Overlay { - private final Drawable arrow[] = new Drawable[18]; + private final Drawable[] arrows; private final int arrowWidth, arrowHeight; private final Drawable statsMarker; private final Drawable waypointMarker; @@ -53,68 +57,94 @@ public class MyTracksOverlay extends Overlay { private final Paint selectedTrackPaint; private final Paint errorCirclePaint; private final Context context; - private final ArrayList waypoints = new ArrayList(); - private final ArrayList points = new ArrayList(1024); + private final List waypoints; + private final List points; + private final BlockingQueue pendingPoints; private boolean trackDrawingEnabled; private int lastHeading = 0; private Location myLocation; 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) { this.context = context; + + this.waypoints = new ArrayList(); + this.points = new ArrayList(1024); + this.pendingPoints = new ArrayBlockingQueue( + MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true); // TODO: Can we use a FrameAnimation or similar here rather // than individual resources for each arrow direction? - - arrow[0] = context.getResources().getDrawable(R.drawable.arrow_0); - arrow[1] = context.getResources().getDrawable(R.drawable.arrow_20); - arrow[2] = context.getResources().getDrawable(R.drawable.arrow_40); - arrow[3] = context.getResources().getDrawable(R.drawable.arrow_60); - arrow[4] = context.getResources().getDrawable(R.drawable.arrow_80); - arrow[5] = context.getResources().getDrawable(R.drawable.arrow_100); - arrow[6] = context.getResources().getDrawable(R.drawable.arrow_120); - arrow[7] = context.getResources().getDrawable(R.drawable.arrow_140); - arrow[8] = context.getResources().getDrawable(R.drawable.arrow_160); - arrow[9] = context.getResources().getDrawable(R.drawable.arrow_180); - arrow[10] = context.getResources().getDrawable(R.drawable.arrow_200); - arrow[11] = context.getResources().getDrawable(R.drawable.arrow_220); - arrow[12] = context.getResources().getDrawable(R.drawable.arrow_240); - arrow[13] = context.getResources().getDrawable(R.drawable.arrow_260); - arrow[14] = context.getResources().getDrawable(R.drawable.arrow_280); - arrow[15] = context.getResources().getDrawable(R.drawable.arrow_300); - arrow[16] = context.getResources().getDrawable(R.drawable.arrow_320); - arrow[17] = context.getResources().getDrawable(R.drawable.arrow_340); - arrowWidth = arrow[lastHeading].getIntrinsicWidth(); - arrowHeight = arrow[lastHeading].getIntrinsicHeight(); - for (int i = 0; i <= 17; i++) { - arrow[i].setBounds(0, 0, arrowWidth, arrowHeight); + final Resources resources = context.getResources(); + arrows = new Drawable[] { + resources.getDrawable(R.drawable.arrow_0), + resources.getDrawable(R.drawable.arrow_20), + resources.getDrawable(R.drawable.arrow_40), + resources.getDrawable(R.drawable.arrow_60), + resources.getDrawable(R.drawable.arrow_80), + resources.getDrawable(R.drawable.arrow_100), + resources.getDrawable(R.drawable.arrow_120), + resources.getDrawable(R.drawable.arrow_140), + resources.getDrawable(R.drawable.arrow_160), + resources.getDrawable(R.drawable.arrow_180), + resources.getDrawable(R.drawable.arrow_200), + resources.getDrawable(R.drawable.arrow_220), + resources.getDrawable(R.drawable.arrow_240), + resources.getDrawable(R.drawable.arrow_260), + resources.getDrawable(R.drawable.arrow_280), + resources.getDrawable(R.drawable.arrow_300), + resources.getDrawable(R.drawable.arrow_320), + resources.getDrawable(R.drawable.arrow_340) + }; + arrowWidth = arrows[lastHeading].getIntrinsicWidth(); + 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(); markerHeight = statsMarker.getIntrinsicHeight(); 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); - endMarker = context.getResources().getDrawable(R.drawable.red_dot); + endMarker = resources.getDrawable(R.drawable.red_dot); endMarker.setBounds(0, 0, markerWidth, markerHeight); - waypointMarker = - context.getResources().getDrawable(R.drawable.blue_pushpin); + waypointMarker = resources.getDrawable(R.drawable.blue_pushpin); waypointMarker.setBounds(0, 0, markerWidth, markerHeight); selectedTrackPaint = new Paint(); - selectedTrackPaint.setColor(context.getResources().getColor(R.color.red)); + selectedTrackPaint.setColor(resources.getColor(R.color.red)); selectedTrackPaint.setStrokeWidth(3); selectedTrackPaint.setStyle(Paint.Style.STROKE); selectedTrackPaint.setAntiAlias(true); errorCirclePaint = new Paint(); - errorCirclePaint.setColor(context.getResources().getColor(R.color.blue)); + errorCirclePaint.setColor(resources.getColor(R.color.blue)); errorCirclePaint.setStyle(Paint.Style.STROKE); errorCirclePaint.setStrokeWidth(3); errorCirclePaint.setAlpha(127); @@ -123,36 +153,51 @@ public class MyTracksOverlay extends Overlay { /** * Add a location to the map overlay. + * + * NOTE: This method doesn't take ownership of the given location, so it is + * safe to reuse the same location while calling this method. * - * NOTE: this method takes ownership of this location and may change it. - * - * @param l the location to add + * @param l the location to add. */ public void addLocation(Location l) { - if (l != null) { - synchronized (points) { - points.add(l); - } - } + // Queue up in the pending queue until it's merged with {@code #points}. + pendingPoints.offer(new CachedLocation(l)); } public void addWaypoint(Waypoint wpt) { - waypoints.add(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 int getNumLocations() { synchronized (points) { - return points.size(); + return points.size() + pendingPoints.size(); + } + } + + // Visible for testing. + int getNumWaypoints() { + synchronized (waypoints) { + return waypoints.size(); + } + } + + public void clearPoints() { + synchronized (points) { + points.clear(); + pendingPoints.clear(); + lastPath = null; + lastViewRect = null; } } public void clearWaypoints() { - waypoints.clear(); - } - - public void clearPoints() { - synchronized (points) { - points.clear(); + synchronized (waypoints) { + waypoints.clear(); } } @@ -170,230 +215,175 @@ public class MyTracksOverlay extends Overlay { return; } + // It's safe to keep projection within a single draw operation. + final Projection projection = getMapProjection(mapView); + // Get the current viewing window. if (trackDrawingEnabled) { + Rect viewRect = getMapViewRect(mapView); + // Draw the selected track: - drawTrack(canvas, mapView); + drawTrack(canvas, projection, viewRect); // Draw the waypoints: - drawWaypoints(canvas, mapView); + drawWaypoints(canvas, projection); } // Draw the current location - drawMyLocation(canvas, mapView); + drawMyLocation(canvas, projection); } - - private void drawWaypoints(Canvas canvas, MapView mapView) { - ArrayList currentWaypoints = waypoints; - for (int i = 1; i < currentWaypoints.size(); i++) { - 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(); - } + + // Visible for testing. + Projection getMapProjection(MapView mapView) { + return mapView.getProjection(); } - - private void drawMyLocation(Canvas canvas, MapView mapView) { - // Draw the arrow icon: - 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: + + // Visible for testing. + Rect getMapViewRect(MapView mapView) { int w = mapView.getLongitudeSpan(); int h = mapView.getLatitudeSpan(); int cx = mapView.getMapCenter().getLongitudeE6(); 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); + } + + // Visible for testing. + Path newPath() { + return new Path(); + } + + // Visible for testing. + Path getLastPath() { + return lastPath; + } - // Global bounding box, including points not visible - int allMinLat, allMinLon, allMaxLat, allMaxLon; - + 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) { - int numPoints = points.size(); - if (numPoints < 2) { - return; - } - - GeoPoint geoPoint; - Location loc; - int minLon, maxLon, minLat, maxLat; - lastValidLocation = points.get(0); - int lastLocLon = allMinLon = allMaxLon = (int) (lastValidLocation.getLongitude() * 1E6); - int lastLocLat = allMinLat = allMaxLat = (int) (lastValidLocation.getLatitude() * 1E6); - lastLocValid = MyTracksUtils.isValidLocation(lastValidLocation); - boolean lastLocVisible = false; - path = new Path(); - - // Loop over track points: - path.incReserve(numPoints); - for (int i = 1; i < numPoints; i++) { - loc = points.get(i); - - boolean locValid = MyTracksUtils.isValidLocation(loc); - boolean locVisible = false; - if (locValid) { - locLon = (int) (loc.getLongitude() * 1E6); - locLat = (int) (loc.getLatitude() * 1E6); - - if (firstGeoPoint == null) { - // Found the starting point - firstGeoPoint = new GeoPoint(locLat, locLon); - } - - // If both the current and previous locations were valid - if (lastLocValid) { - lastValidLocation = loc; - - // Get the bounding box of the segment about to be drawn - if (locLon > lastLocLon) { - minLon = lastLocLon; - maxLon = locLon; - } 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); - } - } + // Merge the pending points with the list of cached locations. + final GeoPoint referencePoint = projection.fromPixels(0, 0); + int newPoints = pendingPoints.drainTo(points); + boolean newProjection = !viewRect.equals(lastViewRect) || + !referencePoint.equals(lastReferencePoint); + if (newPoints == 0 && lastPath != null && !newProjection) { + // No need to recreate path (same points and viewing area). + path = lastPath; + } else { + int numPoints = points.size(); + if (numPoints < 2) { + // Not enough points to draw a path. + path = null; + } else if (lastPath != null && !newProjection) { + // Incremental update of the path, without repositioning the view. + path = lastPath; + updatePath(projection, viewRect, path, numPoints - newPoints); + } else { + // The view has changed so we have to start from scratch. + path = newPath(); + path.incReserve(numPoints); + updatePath(projection, viewRect, path, 0); + } + lastPath = path; + } + lastReferencePoint = referencePoint; + lastViewRect = viewRect; + } + if (path != null) { + canvas.drawPath(path, selectedTrackPaint); + } + + // Draw the "End" marker. + if (showEndMarker) { + for (int i = points.size() - 1; i >= 0; --i) { + if (points.get(i).valid) { + drawElement(canvas, projection, points.get(i).geoPoint, endMarker, + -markerWidth / 2, -markerHeight); + break; } - lastLocLon = locLon; - lastLocLat = locLat; - lastLocValid = locValid; - lastLocVisible = locVisible; } } - - canvas.drawPath(path, selectedTrackPaint); - - if (drawBounds) { - drawBoundingBox(canvas, mapView, - allMinLat, allMinLon, allMaxLat, allMaxLon); - } - - // 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(); + + // Draw the "Start" marker. + for (int i = 0; i < points.size(); ++i) { + if (points.get(i).valid) { + drawElement(canvas, projection, points.get(i).geoPoint, startMarker, + -markerWidth / 2, -markerHeight); + break; + } } } - - private void drawBoundingBox(Canvas canvas, MapView mapView, int allMinLat, - int allMinLon, int allMaxLat, int allMaxLon) { - // Transform coordinates - GeoPoint maxPoint = new GeoPoint(allMaxLat, allMaxLon); - GeoPoint minPoint = new GeoPoint(allMinLat, allMinLon); - Point minPt = new Point(); - Point maxPt = new Point(); - mapView.getProjection().toPixels(minPoint, minPt); - mapView.getProjection().toPixels(maxPoint, maxPt); - Rect allBounds = new Rect(minPt.x, minPt.y, maxPt.x, maxPt.y); - - // Prepare a green paint - Paint boundingBoxPaint = new Paint(); - boundingBoxPaint.setColor(context.getResources().getColor(R.color.green)); - boundingBoxPaint.setStrokeWidth(2); - boundingBoxPaint.setStyle(Paint.Style.STROKE); - boundingBoxPaint.setAntiAlias(true); - - // Disable clipping + + private void updatePath(Projection projection, Rect viewRect, Path path, + int startLocationIdx) { + // Whether to start a new segment on new valid and visible point. + boolean newSegment = + startLocationIdx > 0 ? !points.get(startLocationIdx - 1).valid : true; + boolean lastVisible = !newSegment; + final Point pt = new Point(); + // Loop over track points. + for (int i = startLocationIdx; i < points.size(); ++i) { + CachedLocation loc = points.get(i); + // Check if valid, if not then indicate a new segment. + if (!loc.valid) { + newSegment = true; + continue; + } + + final GeoPoint geoPoint = loc.geoPoint; + // Check if this breaks the existing segment. + boolean visible = alwaysVisible || viewRect.contains( + geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); + if (!visible && !lastVisible) { + // This is a point outside view not connected to a visible one. + newSegment = true; + } + lastVisible = visible; + + // 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.clipRect(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, - Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, - Region.Op.REPLACE); - - // Draw the bounding box - canvas.drawRect(allBounds, boundingBoxPaint); - - // Re-enable clipping + canvas.translate(pt.x + offsetX, pt.y + offsetY); + element.draw(canvas); canvas.restore(); - } - - @Override - public boolean draw(Canvas canvas, MapView mapView, boolean shadow, - long when) { - draw(canvas, mapView, shadow); - return false; + return pt; } /** @@ -436,20 +426,22 @@ public class MyTracksOverlay extends Overlay { final Location tapLocation = MyTracksUtils.getLocation(p); double dmin = Double.MAX_VALUE; Waypoint waypoint = null; - for (int i = 0; i < waypoints.size(); i++) { - final Location waypointLocation = waypoints.get(i).getLocation(); - if (waypointLocation == null) { - continue; - } - final double d = waypointLocation.distanceTo(tapLocation); - if (d < dmin) { - dmin = d; - waypoint = waypoints.get(i); + synchronized (waypoints) { + for (int i = 0; i < waypoints.size(); i++) { + final Location waypointLocation = waypoints.get(i).getLocation(); + if (waypointLocation == null) { + continue; + } + final double d = waypointLocation.distanceTo(tapLocation); + if (d < dmin) { + dmin = d; + waypoint = waypoints.get(i); + } } } - if (waypoint != null - && dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { + if (waypoint != null && + dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { Intent intent = new Intent(context, MyTracksWaypointDetails.class); intent.putExtra("waypointid", waypoint.getId()); context.startActivity(intent); @@ -457,8 +449,4 @@ public class MyTracksOverlay extends Overlay { } return super.onTap(p, mapView); } - - public void setDrawBounds(boolean drawBounds) { - this.drawBounds = drawBounds; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java index 20512ce42..201578f5e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java @@ -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.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 android.content.SharedPreferences; @@ -25,12 +26,10 @@ import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceManager; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; -import android.speech.tts.TextToSpeech; -import android.util.Log; +import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; /** * 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_SPLIT_FREQUENCY = 0; - private static boolean mTTSAvailable; private BackupPreferencesListener backupListener; - 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. */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); // 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 PreferenceManager preferenceManager = getPreferenceManager(); @@ -83,7 +72,7 @@ public class MyTracksSettings extends PreferenceActivity { preferenceManager.setSharedPreferencesMode(0); // Set up automatic preferences backup - backupListener = BackupPreferencesListener.create(this); + backupListener = BackupPreferencesListener.create(this, apiFeatures); preferences = preferenceManager.getSharedPreferences(); preferences.registerOnSharedPreferenceChangeListener(backupListener); @@ -108,7 +97,7 @@ public class MyTracksSettings extends PreferenceActivity { updatePreferenceUnits(metricUnitsPreference.isChecked()); // Disable TTS announcement preference if not available - if (!mTTSAvailable) { + if (!apiFeatures.hasTextToSpeech()) { IntegerListPreference announcementFrequency = (IntegerListPreference) findPreference( getString(R.string.announcement_frequency_key)); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java b/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java index e013e4b04..c4a99ccf2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java @@ -23,7 +23,8 @@ package com.google.android.apps.mytracks; */ public interface ProgressIndicator { - public void setProgressMessage(String text); + public void setProgressMessage(int resId); + public void clearProgressMessage(); public void setProgressValue(int percent); diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index 9eed646a2..403190a6c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -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.TracksColumns; 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 android.app.Activity; @@ -33,7 +35,6 @@ import android.location.LocationManager; import android.location.LocationProvider; import android.os.Bundle; import android.os.Handler; -import android.speech.tts.TextToSpeech; import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; @@ -143,7 +144,9 @@ public class StatsActivity extends Activity super.onCreate(savedInstanceState); // 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: requestWindowFeature(Window.FEATURE_NO_TITLE); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index f8ba71f6a..1ea043aff 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -175,7 +175,7 @@ public interface MyTracksProviderUtils { * @return A cursor over the selected range of locations */ Cursor getWaypointsCursor(long trackId, long minWaypointId, - long maxWaypoints); + int maxWaypoints); /** * Finds a track by given unique track id. @@ -206,32 +206,6 @@ public interface MyTracksProviderUtils { */ 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. * Caller gets to own the returned cursor. Don't forget to close it. @@ -319,7 +293,7 @@ public interface MyTracksProviderUtils { * @return a new location object */ Location createLocation(Cursor cursor); - + /** * Fill a location object with values from a given cursor. * @@ -327,7 +301,7 @@ public interface MyTracksProviderUtils { * @param location a location object to be overwritten */ void fillLocation(Cursor cursor, Location location); - + /** * Creates a waypoint object from a given cursor. * diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 28dd11dd6..ffbd77d9c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -775,7 +775,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Cursor getWaypointsCursor(long trackId, long minWaypointId, - long maxWaypoints) { + int maxWaypoints) { String selection; if (minWaypointId > 0) { selection = String.format("%s=%d AND %s>=%d", @@ -845,67 +845,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { 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 public Cursor getTracksCursor(String selection) { Cursor cursor = context.getContentResolver().query( diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackBuffer.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackBuffer.java deleted file mode 100644 index 9cc5fdead..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackBuffer.java +++ /dev/null @@ -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; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java index a5a89564f..0af4f754a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java @@ -25,7 +25,6 @@ import com.google.android.apps.mytracks.MyTracksSettings; import com.google.android.apps.mytracks.ProgressIndicator; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; 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.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.TripStatistics; @@ -44,144 +43,298 @@ import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.location.Location; -import android.os.Handler; -import android.os.HandlerThread; import android.util.Log; -import org.xmlpull.v1.XmlPullParserException; - import java.io.IOException; import java.util.ArrayList; +import java.util.List; import java.util.Vector; +import org.xmlpull.v1.XmlPullParserException; + /** * A helper class used to transmit tracks to Google MyMaps. + * A new instance should be used for each upload. * * @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 = "http://maps.google.com/mapfiles/ms/micons/red-dot.png"; private static final String START_ICON_URL = "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 AuthManager auth; private final long trackId; private final ProgressIndicator progressIndicator; + private final OnSendCompletedListener onCompletion; private final StringUtils stringUtils; private final MyTracksProviderUtils providerUtils; private String mapId; - private String statusMessage = ""; - private boolean success = true; - private Runnable onCompletion; - - private HandlerThread handlerThread; - private Handler handler; - - private String originalDescription; - + // GData service private MyMapsGDataWrapper wrapper; 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 distances; - private Vector elevations; - private DoubleBuffer elevationBuffer; + public interface OnSendCompletedListener { + void onSendCompleted(String mapId, boolean success, int statusMessage); + } - /** - * Prepares a buffer of locations for transmission to google maps. - * - * @param track the original track with meta data - * @param buffer a buffer of locations on the track - * @return an array of tracks each with a sub section of the points in the - * original buffer - */ - private ArrayList prepareLocations(Track track, - TrackBuffer buffer) { - ArrayList splitTracks = new ArrayList(); + 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); + } - // Create segments from each full track: - Track segment = new Track(); - TripStatistics segmentStats = segment.getStatistics(); - TripStatistics trackStats = track.getStatistics(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - segmentStats.setStartTime(trackStats.getStartTime()); - segmentStats.setStopTime(trackStats.getStopTime()); - boolean startNewTrackSegment = false; - for (int i = 0; i < buffer.getLocationsLoaded(); ++i) { - Location loc = buffer.get(i); - if (loc.getLatitude() > 90) { - startNewTrackSegment = true; + @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("

" + track.getDescription() + "

" + + stringUtils.generateTrackDescription(track, null, null) + "

"); + 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); } - - if (startNewTrackSegment) { - // Close up the last segment. - prepareTrackSegment(segment, splitTracks); - - Log.d(MyTracksConstants.TAG, - "MyTracksSendToMyMaps: Starting new track segment..."); - startNewTrackSegment = false; - segment = new Track(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); + + // Upload all of the segments of the track plus start/end markers + if (success) { + success = uploadAllTrackPoints(track, originalDescription); } - - if (loc.getLatitude() <= 90) { - segment.addLocation(loc); - if (segmentStats.getStartTime() < 0) { - segmentStats.setStartTime(loc.getTime()); + + // 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); + } + } + }); } - - prepareTrackSegment(segment, splitTracks); - - return splitTracks; } /** - * Prepares a track segment for sending to google maps. - * The main steps are: - * - correcting end time - * - decimating locations - * - splitting into smaller tracks. + * Creates a new map for the given track. * - * The final track pieces will be put in the array list splitTracks. - * - * @param segment the original segment of the track - * @param splitTracks an array of smaller track segments + * @param track The track that will be uploaded to this map + * @return True on success. */ - private void prepareTrackSegment( - Track segment, ArrayList splitTracks) { - TripStatistics segmentStats = segment.getStatistics(); - if (segmentStats.getStopTime() < 0 - && segment.getLocations().size() > 0) { - segmentStats.setStopTime(segment.getLocations().size() - 1); + 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); } - /* - * Decimate to 2 meter precision. Mapshop doesn't like too many - * points: - */ - MyTracksUtils.decimate(segment, 2.0); - - /* It the track still has > 500 points, split it in pieces: */ - if (segment.getLocations().size() > 500) { - splitTracks.addAll(MyTracksUtils.split(segment, 500)); - } else if (segment.getLocations().size() >= 2) { - splitTracks.add(segment); + 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 distances = new Vector(); + Vector elevations = new Vector(); + DoubleBuffer elevationBuffer = + new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + + List locations = new ArrayList(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("

" + originalDescription + "

" + + stringUtils.generateTrackDescription( + track, distances, elevations) + + "

"); + 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. @@ -206,216 +359,197 @@ public class SendToMyMaps { } } - 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. + * Builds a placemark MyMapsFeature from a track. * - * @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 + * @param track the track + * @param isStart true if it's the start of the track, or false for end + * @return a MyMapsFeature */ - 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 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 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); + private boolean prepareAndUploadPoints(Track track, List 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; } - - // 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; + + // Prepare/pre-process the points + ArrayList 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)); } - if (totalLocationsRead == 0) { - // Put a marker at the first point of the first valid segment: - uploadMarker(track, buffer.get(0), true); + 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; } - 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 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(); } - + + locations.clear(); + totalLocationsUploaded += numLocations; + updateProgress(); return true; } /** - * Uploads a given list of tracks to Google MyMaps using the maps GData feed. + * Prepares a buffer of locations for transmission to google maps. * - * @param track the track meta-data is used for the map name + * @param track the original track with meta data + * @param buffer a buffer of locations on the track + * @return an array of tracks each with a sub section of the points in the + * original buffer */ - private boolean uploadTrackPoints(final Track track, final Track currentTrack, - int totalLocationsRead, double segmentSize) { - setProgress(track, totalLocationsRead, segmentSize, 0.6666); - progressIndicator.setProgressMessage( - context.getString(R.string.progress_message_sending_mymaps)); + private ArrayList prepareLocations( + Track track, Iterable locations) { + ArrayList splitTracks = new ArrayList(); + + // Create segments from each full track: + Track segment = new Track(); + TripStatistics segmentStats = segment.getStatistics(); + TripStatistics trackStats = track.getStatistics(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(/* track.getDescription() */ ""); + segment.setCategory(track.getCategory()); + segmentStats.setStartTime(trackStats.getStartTime()); + segmentStats.setStopTime(trackStats.getStopTime()); + boolean startNewTrackSegment = false; + for (Location loc : locations) { + if (totalLocationsPrepared % 100 == 0) { + updateProgress(); + } + if (loc.getLatitude() > 90) { + startNewTrackSegment = true; + } + + if (startNewTrackSegment) { + // Close up the last segment. + prepareTrackSegment(segment, splitTracks); + + Log.d(MyTracksConstants.TAG, + "MyTracksSendToMyMaps: Starting new track segment..."); + startNewTrackSegment = false; + segment = new Track(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(/* track.getDescription() */ ""); + segment.setCategory(track.getCategory()); + } + + if (loc.getLatitude() <= 90) { + segment.addLocation(loc); + if (segmentStats.getStartTime() < 0) { + segmentStats.setStartTime(loc.getTime()); + } + } + totalLocationsPrepared++; + } + + prepareTrackSegment(segment, splitTracks); + + return splitTracks; + } + + /** + * Prepares a track segment for sending to google maps. + * The main steps are: + * - correcting end time + * - decimating locations + * - splitting into smaller tracks. + * + * The final track pieces will be put in the array list splitTracks. + * + * @param segment the original segment of the track + * @param splitTracks an array of smaller track segments + */ + private void prepareTrackSegment( + Track segment, ArrayList splitTracks) { + TripStatistics segmentStats = segment.getStatistics(); + if (segmentStats.getStopTime() < 0 + && segment.getLocations().size() > 0) { + segmentStats.setStopTime(segment.getLocations().size() - 1); + } + + /* + * Decimate to 2 meter precision. Mapshop doesn't like too many + * points: + */ + MyTracksUtils.decimate(segment, 2.0); + + /* It the track still has > 500 points, split it in pieces: */ + if (segment.getLocations().size() > 500) { + splitTracks.addAll(MyTracksUtils.split(segment, 500)); + } else if (segment.getLocations().size() >= 2) { + splitTracks.add(segment); + } + } + + /** + * Uploads a given list of tracks to Google MyMaps using the maps GData feed. + */ + private boolean uploadTrackPoints(final Track track) { return wrapper.runQuery(new QueryFunction() { @Override public void query(MapsClient client) throws AuthenticationException, IOException, Exception { String featureFeed = MapsClient.getFeaturesFeed(mapId); Log.d(MyTracksConstants.TAG, "Feature feed url: " + featureFeed); - uploadTrackPoints(track, currentTrack, client, featureFeed); + uploadTrackPoints(track, client, featureFeed); } }); } - /** - * 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, + private boolean uploadTrackPoints(Track splitTrack, MapsClient client, String featureFeed) throws IOException, Exception { Entry entry = null; - if (splitTrack.getId() != track.getId() - || splitTrack.getLocations().size() < 2) { + int numLocations = splitTrack.getLocations().size(); + if (numLocations < 2) { // Need at least two points for a polyline: + Log.w(MyTracksConstants.TAG, "Not uploading too few points"); return true; } - + // Put the line: entry = gdataConverter.getEntryForFeature( buildMyMapsLineFeature(splitTrack)); @@ -435,6 +569,29 @@ public class SendToMyMaps { 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. * @@ -472,7 +629,7 @@ public class SendToMyMaps { } catch (IOException e) { Log.w(MyTracksConstants.TAG, "SendToMyMaps: createEntry 1st try failed. Retrying."); - + // Retry once (often IOException is thrown on a timeout): client.createEntry(featureFeed, auth.getAuthToken(), entry); Log.d(MyTracksConstants.TAG, @@ -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("

" + track.getDescription() + "

" - + stringUtils.generateTrackDescription(track, null, null) + "

"); - wrapper = new MyMapsGDataWrapper(context); - wrapper.setAuthManager(auth); - wrapper.setRetryOnAuthFailure(true); - - distances = new Vector(); - elevations = new Vector(); - 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("

" + originalDescription + "

" - + stringUtils.generateTrackDescription( - track, distances, elevations) - + "

"); - 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. * @@ -695,4 +673,17 @@ public class SendToMyMaps { myMapsFeature.setDescription(wpt.getDescription().replaceAll("\n", "
")); 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); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java index 4c3638c3a..cb49c4a51 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java @@ -18,7 +18,6 @@ package com.google.android.apps.mytracks.io; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; 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.util.FileUtils; import com.google.android.apps.mytracks.util.MyTracksUtils; @@ -254,55 +253,8 @@ public class TrackWriter { void writeDocument() { Log.d(MyTracksConstants.TAG, "Started writing track."); writer.writeHeader(); - TrackBuffer buffer = new TrackBuffer(1024); - Location last = null; - 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); - } + // TODO: Fix ordering (in GPX waypoints should come first) + writeLocations(); writeWaypoints(track.getId()); writer.writeFooter(); writer.close(); @@ -310,4 +262,66 @@ public class TrackWriter { Log.d(MyTracksConstants.TAG, "Done writing track."); 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); + } + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java index bc3c15ddd..55261f8a5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java @@ -102,7 +102,7 @@ public class TrackWriterFactory { * @param format the output format to write in * @return the new track writer */ - public static TrackWriter newWriter(Context context, + private static TrackWriter newWriter(Context context, MyTracksProviderUtils providerUtils, Track track, TrackFileFormat format) { TrackFormatWriter writer = format.newFormatWriter(context); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java index b6caaeab9..51e9439ea 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java @@ -15,7 +15,7 @@ */ 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.content.Context; @@ -66,8 +66,9 @@ public abstract class BackupPreferencesListener /** * Creates and returns a proper instance of the listener for this device. */ - public static BackupPreferencesListener create(Context context) { - if (MyTracksConstants.ANDROID_API_LEVEL >= 8) { + public static BackupPreferencesListener create( + Context context, ApiFeatures apiFeatures) { + if (apiFeatures.hasBackup()) { return new BackupPreferencesListenerImpl(context); } else { return new DummyBackupPreferencesListener(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SafeStatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/SafeStatusAnnouncerTask.java deleted file mode 100644 index c2266978f..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SafeStatusAnnouncerTask.java +++ /dev/null @@ -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() { - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactory.java new file mode 100644 index 000000000..844b48b95 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactory.java @@ -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); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactory.java new file mode 100644 index 000000000..c7ab903fd --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactory.java @@ -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; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java index ba0f0cf52..158a978ae 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java @@ -26,6 +26,7 @@ import com.google.android.maps.mytracks.R; import android.content.Context; import android.content.SharedPreferences; import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnInitListener; import android.util.Log; import java.util.Locale; @@ -35,8 +36,7 @@ import java.util.Locale; * * @author Sandor Dornbush */ -public class StatusAnnouncerTask - implements TextToSpeech.OnInitListener, PeriodicTask { +public class StatusAnnouncerTask implements PeriodicTask { /** * The rate at which announcements are spoken. @@ -56,12 +56,12 @@ public class StatusAnnouncerTask /** * 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. */ - private int ttsStatus = TextToSpeech.ERROR; + private boolean ready = false; /** * Constructs the announcer and start the TTS engine. @@ -70,19 +70,40 @@ public class StatusAnnouncerTask this.context = context; this.stringUtils = new StringUtils(context); - tts = new TextToSpeech(context, this); - // Force the language to be the same as the string we will be speaking. - tts.setLanguage(Locale.getDefault()); - // Slow down the speed just a bit as it is hard to hear when exercising. - tts.setSpeechRate(TTS_SPEECH_RATE); + // We can't have this class also be the listener, otherwise it's unsafe to + // reference it in Cupcake (even if we don't instantiate it). + tts = new TextToSpeech(context, new OnInitListener() { + @Override + public void onInit(int status) { + onTtsInit(status); + } + }); } /** - * Notifies that the tts engine is done with initialization. + * Called when the TTS engine is initialized. */ - public void onInit(int status) { - this.ttsStatus = status; + private void onTtsInit(int 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 public void run(TrackRecordingService service) { - if (ttsStatus != TextToSpeech.SUCCESS && tts != null) { - Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not initialized."); + if (!ready) { + Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready."); return; } @@ -179,4 +200,8 @@ public class StatusAnnouncerTask @Override public void start() { } + + public static int getVolumeStream() { + return TextToSpeech.Engine.DEFAULT_STREAM; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 3104f41b8..1a3da0b1b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -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.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; +import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; @@ -106,7 +107,7 @@ public class TrackRecordingService extends Service implements LocationListener { /** * Status announcer executer. */ - private PeriodicTaskExecuter executer; + private PeriodicTaskExecuter announcementExecuter; private TaskExecuterManager signalManager; private SplitManager splitManager; @@ -181,18 +182,6 @@ public class TrackRecordingService extends Service implements LocationListener { */ 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 */ @@ -645,17 +634,12 @@ public class TrackRecordingService extends Service implements LocationListener { (NotificationManager) getSystemService(NOTIFICATION_SERVICE); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); splitManager = new SplitManager(this); - try { - signalManager = - new TaskExecuterManager(-1, new SignalStrengthTaskModern(this), this); - Log.d(MyTracksConstants.TAG, - "TrackRecordingService using modern signal strength api."); - } catch (LinkageError e) { - Log.w(MyTracksConstants.TAG, - "TrackRecordingService could not load modern signal strength.", e); - signalManager = - new TaskExecuterManager(-1, new SignalStrengthTask(this), this); - } + + SignalStrengthTaskFactory strengthTaskFactory = + new SignalStrengthTaskFactory(ApiFeatures.getInstance()); + signalManager = + new TaskExecuterManager(-1, strengthTaskFactory.create(this), this); + prefManager = new PreferenceManager(this); prefManager.onSharedPreferenceChanged(null); registerLocationListener(); @@ -675,19 +659,23 @@ public class TrackRecordingService extends Service implements LocationListener { } showNotification(); } - + /** * Creates an {@link Executer} and schedules {@class SafeStatusAnnouncerTask}. * The announcer requires a TTS service and user should have enabled * the announcements, otherwise this method is no-op. */ private void setUpAnnouncer() { - if (mTTSAvailable && announcementFrequency != -1) { - if (executer == null) { - SafeStatusAnnouncerTask announcer = new SafeStatusAnnouncerTask(this); - executer = new PeriodicTaskExecuter(announcer, this); + if (announcementFrequency != -1) { + if (announcementExecuter == null) { + StatusAnnouncerFactory statusAnnouncerFactory = + 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; showNotification(); unregisterLocationListener(); - if (executer != null) { - executer.shutdown(); + if (announcementExecuter != null) { + announcementExecuter.shutdown(); } splitManager.shutdown(); super.onDestroy(); @@ -1014,15 +1002,13 @@ public class TrackRecordingService extends Service implements LocationListener { public void setAnnouncementFrequency(int announcementFrequency) { this.announcementFrequency = announcementFrequency; - if (mTTSAvailable) { - if (announcementFrequency == -1) { - if (executer != null) { - executer.shutdown(); - executer = null; - } - } else { - setUpAnnouncer(); + if (announcementFrequency == -1) { + if (announcementExecuter != null) { + announcementExecuter.shutdown(); + announcementExecuter = null; } + } else { + setUpAnnouncer(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java new file mode 100644 index 000000000..b6a4e02fd --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java @@ -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; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java index c2a380e88..6c6ca7af0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java @@ -37,7 +37,7 @@ public class FileUtils { * A set of characters that are prohibited from being in file names. */ private static final Pattern PROHIBITED_CHAR_PATTERN = - Pattern.compile("[^ A-Za-z0-9_.()]+"); + Pattern.compile("[^ A-Za-z0-9_.()-]+"); /** * 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. * * @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 */ String sanitizeName(String name) { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java new file mode 100644 index 000000000..06049f4d7 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java @@ -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> segments = new LinkedList>(); + /** The total number of points in this path. */ + public int totalPoints; + + private List 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(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). + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java index 2d9c2ff54..12542199b 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java @@ -2,21 +2,23 @@ package com.google.android.apps.mytracks.io; +import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.leq; import static org.easymock.EasyMock.same; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; 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.testing.TestingProviderUtilsFactory; import android.content.Context; -import android.database.Cursor; +import android.database.MatrixCursor; import android.location.Location; +import android.provider.BaseColumns; import android.test.AndroidTestCase; import java.io.ByteArrayOutputStream; @@ -25,6 +27,7 @@ import java.io.OutputStream; import org.easymock.EasyMock; import org.easymock.IAnswer; +import org.easymock.IArgumentMatcher; import org.easymock.IMocksControl; /** @@ -226,6 +229,8 @@ public class TrackWriterTest extends AndroidTestCase { expect(providerUtils.getWaypointsCursor( TRACK_ID, 0, MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS)) .andStubReturn(null); + expect(providerUtils.getLocationsCursor( + eq(TRACK_ID), leq(0L), leq(0), eq(false))).andStubReturn(null); // Set expected mock behavior formatWriter.writeHeader(); @@ -242,61 +247,78 @@ public class TrackWriterTest extends AndroidTestCase { public void testWriteDocument() { writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); - Location l1 = new Location("fake1"); - Location l2 = new Location("fake2"); - Location l3 = new Location("fake3"); - Location l4 = new Location("fake4"); - Location l5 = new Location("fake5"); - Location l6 = new Location("fake6"); - Waypoint p1 = new Waypoint(); - Waypoint p2 = new Waypoint(); + final Location[] locs = { + new Location("fake0"), + new Location("fake1"), + new Location("fake2"), + new Location("fake3"), + new Location("fake4"), + new Location("fake5"), + }; + Waypoint[] wps = { new Waypoint(), new Waypoint(), new Waypoint() }; - addLocations(l1, l2, l3, l4, l5, l6); - stubBufferFill( - new Location[] { l1, l2, l3, l4 }, - new Location[] { l5, l6 }); - - track.setStopId(6L); + // Fill locations with valid values + fillLocations(locs); // 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() { + @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 formatWriter.writeHeader(); - formatWriter.writeBeginTrack(l1); + formatWriter.writeBeginTrack(locEq(locs[0])); // Write locations 1-2 formatWriter.writeOpenSegment(); - formatWriter.writeLocation(l1); - formatWriter.writeLocation(l2); + formatWriter.writeLocation(locEq(locs[0])); + formatWriter.writeLocation(locEq(locs[1])); formatWriter.writeCloseSegment(); // Location 3 is not written - it's invalid // Write locations 4-6 formatWriter.writeOpenSegment(); - formatWriter.writeLocation(l4); - formatWriter.writeLocation(l5); - formatWriter.writeLocation(l6); + formatWriter.writeLocation(locEq(locs[3])); + formatWriter.writeLocation(locEq(locs[4])); + formatWriter.writeLocation(locEq(locs[5])); formatWriter.writeCloseSegment(); // End the track - formatWriter.writeEndTrack(l6); + formatWriter.writeEndTrack(locEq(locs[5])); - // Expect reading/writing of the waypoints - Cursor cursor = mocksControl.createMock(Cursor.class); - expect(providerUtils.getWaypointsCursor( - 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(); + // Expect reading/writing of the waypoints (except the first) + formatWriter.writeWaypoint(wps[1]); + formatWriter.writeWaypoint(wps[2]); formatWriter.writeFooter(); formatWriter.close(); @@ -308,7 +330,47 @@ public class TrackWriterTest extends AndroidTestCase { 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 IAnswer stubCursorToArray( + final MatrixCursor cursor, final T[] values) { + return new IAnswer() { + @Override + public T answer() throws Throwable { + return values[cursor.getPosition()]; + } + }; + } + + private void fillLocations(Location... locs) { assertTrue(locs.length < 90); for (int i = 0; i < locs.length; i++) { Location location = locs[i]; @@ -316,46 +378,4 @@ public class TrackWriterTest extends AndroidTestCase { 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() { - 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; - } - }); - } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactoryTest.java new file mode 100644 index 000000000..e37684f64 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactoryTest.java @@ -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); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactoryTest.java new file mode 100644 index 000000000..304002a8d --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactoryTest.java @@ -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()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java new file mode 100644 index 000000000..1f120cbf2 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java @@ -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()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java index 715370bab..01b44c6b6 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java @@ -31,8 +31,8 @@ import junit.framework.TestCase; * @author Rodrigo Damazio */ public class FileUtilsTest extends TestCase { - 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 ORIGINAL_NAME = "Swim\10ming-^across: the/ pacific (ocean)."; + private static final String SANITIZED_NAME = "Swimming-across the pacific (ocean)."; private FileUtils fileUtils; private Set existingFiles;