diff --git a/docs/mock-locations-routes.md b/docs/mock-locations-routes.md index a5787f8..ac6d6a5 100644 --- a/docs/mock-locations-routes.md +++ b/docs/mock-locations-routes.md @@ -1,8 +1,8 @@ -#Mock Locations +# Mock Locations With Lost you can mock not just individual locations but also entire routes. By loading a [GPX trace file](http://www.topografix.com/gpx.asp) onto the device you can configure Lost to replay locations from the trace file including latitude, longitude, speed, and bearing. -**Mocking a single location** +## Mocking a single location To mock a single location with Lost you must first enable mock mode. Then you simply create a mock location object and pass it to the API. ```java @@ -16,11 +16,28 @@ LocationServices.FusedLocationApi.setMockLocation(mockLocation); The mock location object you set will be immediately returned to all registered listeners and will be returned the next time `getLastLocation()` is called. -**Mocking an entire route** -To mock an entire route you must first transfer a [GPX trace file](http://www.topografix.com/gpx.asp) to the device using [adb](http://developer.android.com/tools/help/adb.html). Sample GPX traces can be found on the [public GPS traces page](http://www.openstreetmap.org/traces) for OpenStreetMap. Once the trace file is loaded on the device you can tell Lost to replay the locations in the trace at the requested update interval. +## Mocking an entire route +To mock an entire route you must first transfer a [GPX trace file](http://www.topografix.com/gpx.asp) to the device using [adb](http://developer.android.com/tools/help/adb.html). Sample GPX traces can be found on the [public GPS traces page](http://www.openstreetmap.org/traces) for OpenStreetMap. + +### Install GPX File +To install your gpx file onto your device, run the `install-gpx-trace.sh` script provided. + +Example: + +```bash +install-gpx-trace.sh lost.gpx com.example.myapp +``` + +Once the trace file is loaded on the device you can tell Lost to replay the locations in the trace by issuing a `LocationRequest`. Mock locations will be emitted according to the fastest interval value. ```java -File file = new File(Environment.getExternalStorageDirectory(), "mock_track.gpx"); +File file = new File(context.getExternalFilesDir(null), "lost.gpx"); LocationServices.FusedLocationApi.setMockMode(true); LocationServices.FusedLocationApi.setMockTrace(file); +LocationRequest locationRequest = LocationRequest.create() + .setInterval(1000) + .setFastestInterval(1000) // Mock locations will replay at this interval. + .setSmallestDisplacement(0) + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); +FusedLocationApi.requestLocationUpdates(lostApiClient, locationRequest, locationListener); ``` diff --git a/lost-sample/src/main/java/com/example/lost/LocationListenerActivity.java b/lost-sample/src/main/java/com/example/lost/LocationListenerActivity.java index 48ab753..ddc0d14 100644 --- a/lost-sample/src/main/java/com/example/lost/LocationListenerActivity.java +++ b/lost-sample/src/main/java/com/example/lost/LocationListenerActivity.java @@ -150,22 +150,25 @@ private boolean isMockModePrefEnabled() { if (prefs.getBoolean(getString(R.string.mock_mode_gpx_key), false)) { String filename = prefs.getString(getString(R.string.mock_gpx_file_key), null); - File file = new File(Environment.getExternalStorageDirectory(), filename); + File file = new File(getExternalFilesDir(null), filename); FusedLocationApi.setMockTrace(client, file); } + final Resources res = getResources(); + final String intervalKey = getString(R.string.interval_key); final String displacementKey = getString(R.string.displacement_key); final String priorityKey = getString(R.string.priority_key); - final Resources res = getResources(); + final int intervalDefaultValue = res.getInteger(R.integer.interval_default_value); + final int displacementDefaultValue = res.getInteger(R.integer.displacement_default_value); + final int priorityDefaultValue = res.getInteger(R.integer.priority_default_value); + final LocationRequest locationRequest = LocationRequest.create() - .setInterval( - prefs.getInt(intervalKey, res.getInteger(R.integer.interval_default_value))) - .setSmallestDisplacement( - prefs.getInt(displacementKey, res.getInteger(R.integer.displacement_default_value))) - .setPriority( - prefs.getInt(priorityKey, res.getInteger(R.integer.priority_default_value))); + .setInterval(prefs.getInt(intervalKey, intervalDefaultValue)) + .setFastestInterval(prefs.getInt(intervalKey, intervalDefaultValue)) + .setSmallestDisplacement(prefs.getInt(displacementKey, displacementDefaultValue)) + .setPriority(prefs.getInt(priorityKey, priorityDefaultValue)); FusedLocationApi.requestLocationUpdates(client, locationRequest, listener); diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImpl.java b/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImpl.java index 0059e03..5811cf0 100644 --- a/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImpl.java +++ b/lost/src/main/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImpl.java @@ -175,7 +175,7 @@ private void toggleMockMode() { mockMode = !mockMode; locationEngine.setRequest(null); if (mockMode) { - locationEngine = new MockEngine(context, this); + locationEngine = new MockEngine(context, this, new GpxTraceThreadFactory()); } else { locationEngine = new FusionEngine(context, this); } @@ -186,5 +186,4 @@ private void notifyLocationAvailabilityChanged() { final LocationAvailability availability = locationEngine.createLocationAvailability(); clientManager.notifyLocationAvailability(availability); } - } diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/GpxTraceThreadFactory.java b/lost/src/main/java/com/mapzen/android/lost/internal/GpxTraceThreadFactory.java new file mode 100644 index 0000000..1f6edc2 --- /dev/null +++ b/lost/src/main/java/com/mapzen/android/lost/internal/GpxTraceThreadFactory.java @@ -0,0 +1,12 @@ +package com.mapzen.android.lost.internal; + +import android.content.Context; + +import java.io.File; + +public class GpxTraceThreadFactory implements TraceThreadFactory { + @Override public TraceThread createTraceThread(Context context, File traceFile, MockEngine engine, + SleepFactory sleepFactory) { + return new TraceThread(context, traceFile, engine, sleepFactory); + } +} diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/LocationRequestUnbundled.java b/lost/src/main/java/com/mapzen/android/lost/internal/LocationRequestUnbundled.java index c1d0d65..57fa8af 100644 --- a/lost/src/main/java/com/mapzen/android/lost/internal/LocationRequestUnbundled.java +++ b/lost/src/main/java/com/mapzen/android/lost/internal/LocationRequestUnbundled.java @@ -6,7 +6,7 @@ import java.util.List; /** - * + * Set of location requests used to synthesize parameters for location updates. */ public class LocationRequestUnbundled { diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/MockEngine.java b/lost/src/main/java/com/mapzen/android/lost/internal/MockEngine.java index f3b6dbf..4615785 100644 --- a/lost/src/main/java/com/mapzen/android/lost/internal/MockEngine.java +++ b/lost/src/main/java/com/mapzen/android/lost/internal/MockEngine.java @@ -1,24 +1,9 @@ package com.mapzen.android.lost.internal; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - import android.content.Context; import android.location.Location; -import android.os.Handler; import java.io.File; -import java.io.IOException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; /** * Mock implementation of LocationEngine that reports single locations and/or full GPX traces. @@ -26,18 +11,15 @@ public class MockEngine extends LocationEngine { public static final String MOCK_PROVIDER = "mock"; - // GPX tags - public static final String TAG_TRACK_POINT = "trkpt"; - public static final String TAG_SPEED = "speed"; - public static final String TAG_LAT = "lat"; - public static final String TAG_LNG = "lon"; - private Location location; private File traceFile; + private TraceThreadFactory traceThreadFactory; protected TraceThread traceThread; - public MockEngine(Context context, FusionEngine.Callback callback) { + public MockEngine(Context context, FusionEngine.Callback callback, + TraceThreadFactory traceThreadFactory) { super(context, callback); + this.traceThreadFactory = traceThreadFactory; } @Override public Location getLastLocation() { @@ -50,7 +32,8 @@ public MockEngine(Context context, FusionEngine.Callback callback) { @Override protected void enable() { if (traceFile != null) { - traceThread = new TraceThread(); + traceThread = traceThreadFactory.createTraceThread(getContext(), traceFile, this, + new ThreadSleepFactory()); traceThread.start(); } } @@ -82,91 +65,4 @@ public void setTrace(File file) { public File getTrace() { return traceFile; } - - protected class TraceThread extends Thread { - private boolean canceled; - private Location previous; - - public void cancel() { - canceled = true; - interrupt(); - } - - @Override public void run() { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - final XPath xPath = XPathFactory.newInstance().newXPath(); - final String expression = "//" + TAG_TRACK_POINT; - final String speedExpression = "//" + TAG_SPEED; - - NodeList nodeList = null; - NodeList speedList = null; - try { - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(traceFile); - nodeList = (NodeList) xPath.compile(expression).evaluate(document, XPathConstants.NODESET); - speedList = - (NodeList) xPath.compile(speedExpression).evaluate(document, XPathConstants.NODESET); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (XPathExpressionException e) { - e.printStackTrace(); - } - - parse(nodeList, speedList); - } - - private void parse(NodeList nodeList, NodeList speedList) { - if (nodeList != null) { - for (int i = 0; i < nodeList.getLength(); i++) { - postMockLocation(nodeToLocation(nodeList, speedList, i)); - sleepFastestInterval(); - } - } - } - - private Location nodeToLocation(NodeList nodeList, NodeList speedList, int i) { - final Node node = nodeList.item(i); - String lat = node.getAttributes().getNamedItem(TAG_LAT).getNodeValue(); - String lng = node.getAttributes().getNamedItem(TAG_LNG).getNodeValue(); - - final Location location = new Location(MOCK_PROVIDER); - location.setLatitude(Double.parseDouble(lat)); - location.setLongitude(Double.parseDouble(lng)); - location.setTime(System.currentTimeMillis()); - if (speedList.item(i) != null && speedList.item(i).getFirstChild() != null) { - location.setSpeed(Float.parseFloat(speedList.item(i).getFirstChild().getNodeValue())); - } - - if (previous != null) { - location.setBearing(previous.bearingTo(location)); - } - - previous = location; - return location; - } - - private void sleepFastestInterval() { - if (getRequest() != null) { - try { - Thread.sleep(getRequest().getFastestInterval()); - } catch (InterruptedException e) { - canceled = true; - } - } - } - - private void postMockLocation(final Location mockLocation) { - new Handler(getContext().getMainLooper()).post(new Runnable() { - @Override public void run() { - if (!canceled) { - setLocation(mockLocation); - } - } - }); - } - } } diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/SleepFactory.java b/lost/src/main/java/com/mapzen/android/lost/internal/SleepFactory.java new file mode 100644 index 0000000..eae28b7 --- /dev/null +++ b/lost/src/main/java/com/mapzen/android/lost/internal/SleepFactory.java @@ -0,0 +1,5 @@ +package com.mapzen.android.lost.internal; + +interface SleepFactory { + void sleep(long millis); +} diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/ThreadSleepFactory.java b/lost/src/main/java/com/mapzen/android/lost/internal/ThreadSleepFactory.java new file mode 100644 index 0000000..52697e6 --- /dev/null +++ b/lost/src/main/java/com/mapzen/android/lost/internal/ThreadSleepFactory.java @@ -0,0 +1,11 @@ +package com.mapzen.android.lost.internal; + +class ThreadSleepFactory implements SleepFactory { + @Override public void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/TraceThread.java b/lost/src/main/java/com/mapzen/android/lost/internal/TraceThread.java new file mode 100644 index 0000000..4b72eb5 --- /dev/null +++ b/lost/src/main/java/com/mapzen/android/lost/internal/TraceThread.java @@ -0,0 +1,127 @@ +package com.mapzen.android.lost.internal; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import android.content.Context; +import android.location.Location; +import android.os.Handler; + +import java.io.File; +import java.io.IOException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +class TraceThread extends Thread { + // GPX tags + private static final String TAG_TRACK_POINT = "trkpt"; + private static final String TAG_SPEED = "speed"; + private static final String TAG_LAT = "lat"; + private static final String TAG_LNG = "lon"; + + private boolean canceled; + private Location previous; + + private final Context context; + private final File traceFile; + private final MockEngine engine; + private final SleepFactory sleepFactory; + + TraceThread(Context context, File traceFile, MockEngine engine, SleepFactory sleepFactory) { + this.context = context; + this.traceFile = traceFile; + this.engine = engine; + this.sleepFactory = sleepFactory; + } + + public void cancel() { + canceled = true; + interrupt(); + } + + public boolean isCanceled() { + return canceled; + } + + @Override public void run() { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final XPath xPath = XPathFactory.newInstance().newXPath(); + final String expression = "//" + TAG_TRACK_POINT; + final String speedExpression = "//" + TAG_SPEED; + + NodeList nodeList = null; + NodeList speedList = null; + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(traceFile); + nodeList = (NodeList) xPath.compile(expression).evaluate(document, XPathConstants.NODESET); + speedList = + (NodeList) xPath.compile(speedExpression).evaluate(document, XPathConstants.NODESET); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + } catch (SAXException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (XPathExpressionException e) { + e.printStackTrace(); + } + + parse(nodeList, speedList); + } + + private void parse(NodeList nodeList, NodeList speedList) { + if (nodeList != null) { + for (int i = 0; i < nodeList.getLength(); i++) { + postMockLocation(nodeToLocation(nodeList, speedList, i)); + sleepFastestInterval(); + } + } + } + + private Location nodeToLocation(NodeList nodeList, NodeList speedList, int i) { + final Node node = nodeList.item(i); + String lat = node.getAttributes().getNamedItem(TAG_LAT).getNodeValue(); + String lng = node.getAttributes().getNamedItem(TAG_LNG).getNodeValue(); + + final Location location = new Location(MockEngine.MOCK_PROVIDER); + location.setLatitude(Double.parseDouble(lat)); + location.setLongitude(Double.parseDouble(lng)); + location.setTime(System.currentTimeMillis()); + if (speedList.item(i) != null && speedList.item(i).getFirstChild() != null) { + location.setSpeed(Float.parseFloat(speedList.item(i).getFirstChild().getNodeValue())); + } + + if (previous != null) { + location.setBearing(previous.bearingTo(location)); + } + + previous = location; + return location; + } + + private void sleepFastestInterval() { + final LocationRequestUnbundled request = engine.getRequest(); + if (request != null) { + sleepFactory.sleep(request.getFastestInterval()); + } + } + + private void postMockLocation(final Location mockLocation) { + new Handler(context.getMainLooper()).post(new Runnable() { + @Override public void run() { + if (!canceled) { + engine.setLocation(mockLocation); + } + } + }); + } +} diff --git a/lost/src/main/java/com/mapzen/android/lost/internal/TraceThreadFactory.java b/lost/src/main/java/com/mapzen/android/lost/internal/TraceThreadFactory.java new file mode 100644 index 0000000..085fdbf --- /dev/null +++ b/lost/src/main/java/com/mapzen/android/lost/internal/TraceThreadFactory.java @@ -0,0 +1,10 @@ +package com.mapzen.android.lost.internal; + +import android.content.Context; + +import java.io.File; + +public interface TraceThreadFactory { + TraceThread createTraceThread(Context context, File traceFile, MockEngine engine, + SleepFactory sleepFactory); +} diff --git a/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImplTest.java b/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImplTest.java index 1590835..f7c4210 100644 --- a/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImplTest.java +++ b/lost/src/test/java/com/mapzen/android/lost/internal/FusedLocationProviderServiceImplTest.java @@ -15,7 +15,6 @@ import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricGradleTestRunner; @@ -323,7 +322,6 @@ private void mockService() { assertThat(listener.getMostRecentLocation()).isEqualTo(mockLocation); } - @Test @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") public void setMockTrace_shouldInvokeListenerForEachLocation() throws Exception { api.setMockMode(client, true); api.setMockTrace(client, getTestGpxTrace()); @@ -342,7 +340,6 @@ public void setMockTrace_shouldInvokeListenerForEachLocation() throws Exception assertThat(listener.getAllLocations().get(2).getLongitude()).isEqualTo(2.1); } - @Test @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") public void setMockTrace_shouldBroadcastSpeedWithLocation() throws Exception { api.setMockMode(client, true); api.setMockTrace(client, getTestGpxTrace()); @@ -357,7 +354,6 @@ public void setMockTrace_shouldBroadcastSpeedWithLocation() throws Exception { assertThat(listener.getAllLocations().get(2).getSpeed()).isEqualTo(30f); } - @Test @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") public void setMockTrace_shouldRespectFastestInterval() throws Exception { api.setMockMode(client, true); api.setMockTrace(client, getTestGpxTrace()); diff --git a/lost/src/test/java/com/mapzen/android/lost/internal/MockEngineTest.java b/lost/src/test/java/com/mapzen/android/lost/internal/MockEngineTest.java index 3849cbb..28319cb 100644 --- a/lost/src/test/java/com/mapzen/android/lost/internal/MockEngineTest.java +++ b/lost/src/test/java/com/mapzen/android/lost/internal/MockEngineTest.java @@ -7,34 +7,34 @@ import com.google.common.io.Files; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowEnvironment; -import org.robolectric.shadows.ShadowLooper; +import android.content.Context; import android.location.Location; import android.os.Environment; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.util.ArrayList; import static org.fest.assertions.api.Assertions.assertThat; import static org.robolectric.RuntimeEnvironment.application; -@RunWith(RobolectricGradleTestRunner.class) +@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21, manifest = Config.NONE) public class MockEngineTest { private MockEngine mockEngine; private TestCallback callback; + private TestTraceThreadFactory traceThreadFactory; @Before public void setUp() throws Exception { callback = new TestCallback(); - mockEngine = new MockEngine(application, callback); + traceThreadFactory = new TestTraceThreadFactory(); + mockEngine = new MockEngine(application, callback, traceThreadFactory); } @Test public void shouldNotBeNull() throws Exception { @@ -47,97 +47,11 @@ public class MockEngineTest { assertThat(callback.lastLocation).isEqualTo(mockLocation); } - @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") - @Test public void setTrace_shouldReportEachLocation() throws Exception { - mockEngine.setTrace(getTestGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(0)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations).hasSize(3); - assertThat(callback.locations.get(0).getLatitude()).isEqualTo(0.0); - assertThat(callback.locations.get(0).getLongitude()).isEqualTo(0.1); - assertThat(callback.locations.get(1).getLatitude()).isEqualTo(1.0); - assertThat(callback.locations.get(1).getLongitude()).isEqualTo(1.1); - assertThat(callback.locations.get(2).getLatitude()).isEqualTo(2.0); - assertThat(callback.locations.get(2).getLongitude()).isEqualTo(2.1); - } - - @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") - @Test public void setTrace_shouldReportSpeed() throws Exception { - mockEngine.setTrace(getTestGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(0)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations.get(0).getSpeed()).isEqualTo(10f); - assertThat(callback.locations.get(1).getSpeed()).isEqualTo(20f); - assertThat(callback.locations.get(2).getSpeed()).isEqualTo(30f); - } - - @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") - @Test public void setTrace_shouldCalculateBearing() throws Exception { - mockEngine.setTrace(getTestGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(0)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations.get(0).getBearing()).isEqualTo(0.0f); - assertThat(callback.locations.get(1).getBearing()).isEqualTo( - callback.locations.get(0).bearingTo(callback.locations.get(1))); - assertThat(callback.locations.get(2).getBearing()).isEqualTo( - callback.locations.get(1).bearingTo(callback.locations.get(2))); - } - - @Test public void setTrace_shouldSetHasBearing() throws Exception { - mockEngine.setTrace(getTestGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(0)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations.get(0).hasBearing()).isFalse(); - assertThat(callback.locations.get(1).hasBearing()).isTrue(); - assertThat(callback.locations.get(2).hasBearing()).isTrue(); - } - - @Test public void setTrace_shouldRespectFastestInterval() throws Exception { + @Test public void disable_shouldCancelTraceReplay() throws Exception { mockEngine.setTrace(getTestGpxTrace()); mockEngine.setRequest(LocationRequest.create().setFastestInterval(100)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations).hasSize(1); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations).hasSize(2); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations).hasSize(3); - } - - @Test @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") - public void setTrace_shouldNotRequireSpeed() throws Exception { - mockEngine.setTrace(getTestGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(0)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations.get(0).hasSpeed()).isTrue(); - mockEngine.disable(); - callback.reset(); - Thread.sleep(100); - mockEngine.setTrace(getNoSpeedGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(0)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations.get(0).hasSpeed()).isFalse(); - } - - @Test @Ignore("Intermittently failing. Find a better way to test without Thread.sleep(100)") - public void disable_shouldCancelTraceReplay() throws Exception { - mockEngine.setTrace(getTestGpxTrace()); - mockEngine.setRequest(LocationRequest.create().setFastestInterval(100)); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations).hasSize(1); mockEngine.disable(); - Thread.sleep(100); - ShadowLooper.runUiThreadTasks(); - assertThat(callback.locations).hasSize(1); + assertThat(traceThreadFactory.traceThread.isCanceled()).isTrue(); } public static File getGpxFile(String filename) throws IOException { @@ -161,11 +75,9 @@ public static File getNoSpeedGpxTrace() throws IOException { class TestCallback implements LocationEngine.Callback { private Location lastLocation; - private ArrayList locations = new ArrayList<>(); @Override public void reportLocation(Location location) { lastLocation = location; - locations.add(location); } @Override public void reportProviderDisabled(String provider) { @@ -173,10 +85,21 @@ class TestCallback implements LocationEngine.Callback { @Override public void reportProviderEnabled(String provider) { } + } + + static class TestTraceThreadFactory implements TraceThreadFactory { + private TestTraceThread traceThread; + + @Override public TraceThread createTraceThread(Context context, File traceFile, + MockEngine engine, SleepFactory sleepFactory) { + traceThread = new TestTraceThread(context, traceFile, engine, sleepFactory); + return traceThread; + } + } - public void reset() { - lastLocation = null; - locations.clear(); + static class TestTraceThread extends TraceThread { + TestTraceThread(Context context, File traceFile, MockEngine engine, SleepFactory sleepFactory) { + super(context, traceFile, engine, sleepFactory); } } } diff --git a/lost/src/test/java/com/mapzen/android/lost/internal/TraceThreadTest.java b/lost/src/test/java/com/mapzen/android/lost/internal/TraceThreadTest.java new file mode 100644 index 0000000..d2e49c4 --- /dev/null +++ b/lost/src/test/java/com/mapzen/android/lost/internal/TraceThreadTest.java @@ -0,0 +1,158 @@ +package com.mapzen.android.lost.internal; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowEnvironment; + +import android.location.Location; +import android.os.Environment; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.robolectric.RuntimeEnvironment.application; + +@RunWith(RobolectricTestRunner.class) public class TraceThreadTest { + private TestMockEngine mockEngine; + private TestSleepFactory sleepFactory; + private TraceThread traceThread; + + @Before public void setUp() throws Exception { + mockEngine = new TestMockEngine(); + sleepFactory = new TestSleepFactory(); + traceThread = new TraceThread(application, getTestGpxTrace(), mockEngine, sleepFactory); + } + + @Test public void shouldNotBeNull() throws Exception { + assertThat(traceThread).isNotNull(); + } + + @Test public void shouldReportEachLocation() throws Exception { + traceThread.run(); + assertThat(mockEngine.locations).hasSize(3); + assertThat(mockEngine.locations.get(0).getLatitude()).isEqualTo(0.0); + assertThat(mockEngine.locations.get(0).getLongitude()).isEqualTo(0.1); + assertThat(mockEngine.locations.get(1).getLatitude()).isEqualTo(1.0); + assertThat(mockEngine.locations.get(1).getLongitude()).isEqualTo(1.1); + assertThat(mockEngine.locations.get(2).getLatitude()).isEqualTo(2.0); + assertThat(mockEngine.locations.get(2).getLongitude()).isEqualTo(2.1); + } + + @Test public void shouldReportSpeed() throws Exception { + traceThread.run(); + assertThat(mockEngine.locations.get(0).getSpeed()).isEqualTo(10f); + assertThat(mockEngine.locations.get(1).getSpeed()).isEqualTo(20f); + assertThat(mockEngine.locations.get(2).getSpeed()).isEqualTo(30f); + } + + @Test public void shouldCalculateBearing() throws Exception { + traceThread.run(); + assertThat(mockEngine.locations.get(0).getBearing()).isEqualTo(0.0f); + assertThat(mockEngine.locations.get(1).getBearing()) + .isEqualTo(mockEngine.locations.get(0).bearingTo(mockEngine.locations.get(1))); + assertThat(mockEngine.locations.get(2).getBearing()) + .isEqualTo(mockEngine.locations.get(1).bearingTo(mockEngine.locations.get(2))); + } + + @Test public void shouldSetHasBearing() throws Exception { + traceThread.run(); + assertThat(mockEngine.locations.get(0).hasBearing()).isFalse(); + assertThat(mockEngine.locations.get(1).hasBearing()).isTrue(); + assertThat(mockEngine.locations.get(2).hasBearing()).isTrue(); + } + + @Test public void shouldRespectFastestInterval() throws Exception { + mockEngine.setRequest(new TestLocationRequestUnbundled(100)); + traceThread.run(); + assertThat(sleepFactory.sleepTimeInMillis).isEqualTo(300); + } + + @Test public void shouldReportSpeedIfAvailable() throws Exception { + traceThread = new TraceThread(application, getTestGpxTrace(), mockEngine, sleepFactory); + traceThread.run(); + assertThat(mockEngine.locations.get(0).hasSpeed()).isTrue(); + } + + @Test public void shouldNotReportSpeedIfNotAvailable() throws Exception { + traceThread = new TraceThread(application, getNoSpeedGpxTrace(), mockEngine, sleepFactory); + traceThread.run(); + assertThat(mockEngine.locations.get(0).hasSpeed()).isFalse(); + } + + public static File getTestGpxTrace() throws IOException { + return getGpxFile("lost.gpx"); + } + + public static File getNoSpeedGpxTrace() throws IOException { + return getGpxFile("lost-no-speed.gpx"); + } + + public static File getGpxFile(String filename) throws IOException { + String contents = Files.toString(new File("src/test/resources/" + filename), Charsets.UTF_8); + ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED); + File directory = Environment.getExternalStorageDirectory(); + File file = new File(directory, filename); + FileWriter fileWriter = new FileWriter(file, false); + fileWriter.write(contents); + fileWriter.close(); + return file; + } + + class TestMockEngine extends MockEngine { + private ArrayList locations = new ArrayList<>(); + private LocationRequestUnbundled request = new TestLocationRequestUnbundled(0); + + public TestMockEngine() { + super(application, new FusionEngine.Callback() { + @Override public void reportLocation(Location location) { + } + + @Override public void reportProviderDisabled(String provider) { + } + + @Override public void reportProviderEnabled(String provider) { + } + }, new MockEngineTest.TestTraceThreadFactory()); + } + + @Override public void setLocation(Location location) { + this.locations.add(location); + } + + @Override protected LocationRequestUnbundled getRequest() { + return request; + } + + public void setRequest(LocationRequestUnbundled request) { + this.request = request; + } + } + + class TestLocationRequestUnbundled extends LocationRequestUnbundled { + private long fastestInterval; + + TestLocationRequestUnbundled(long fastestInterval) { + this.fastestInterval = fastestInterval; + } + + @Override public long getFastestInterval() { + return fastestInterval; + } + } + + class TestSleepFactory implements SleepFactory { + private long sleepTimeInMillis; + + @Override public void sleep(long millis) { + sleepTimeInMillis += millis; + } + } +} diff --git a/scripts/install-gpx-trace.sh b/scripts/install-gpx-trace.sh new file mode 100755 index 0000000..42b999a --- /dev/null +++ b/scripts/install-gpx-trace.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# This script installs a GPX trace file onto an emulator or device. The specified trace file is +# copied to the app's external files directory (determined by the provided package name). +# +# Usage: +# install-gpx-trace.sh +# +# Example: +# install-gpx-trace.sh lost.gpx com.example.myapp + +if [[ $# -eq 0 ]]; then + echo "Usage: ${0} " + exit 1 +fi + +TRACE_FILE=$1 + +if [[ -z "$2" ]]; then + PACKAGE_NAME="com.example.lost" +else + PACKAGE_NAME=$2 +fi + +adb push "$1" /sdcard/Android/data/"$PACKAGE_NAME"/files/