Autoformatted all classes (non-testing).

This commit is contained in:
Dennis Guse
2019-07-23 22:22:17 +02:00
parent 5cfae845b7
commit 363cb14bff
141 changed files with 21719 additions and 21793 deletions
@@ -38,63 +38,63 @@ import java.util.List;
* @author Youtao Liu * @author Youtao Liu
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class BootReceiverTest { public class BootReceiverTest {
private static final String SERVICE_NAME = "com.google.android.apps.mytracks.services.TrackRecordingService"; private static final String SERVICE_NAME = "com.google.android.apps.mytracks.services.TrackRecordingService";
/** /**
* Tests the behavior when receive notification which is the phone boot. * Tests the behavior when receive notification which is the phone boot.
*/ */
@Test @Test
public void testOnReceive_startService() { public void testOnReceive_startService() {
// Make sure no TrackRecordingService // Make sure no TrackRecordingService
Intent stopIntent = new Intent(InstrumentationRegistry.getInstrumentation().getContext(), TrackRecordingService.class); Intent stopIntent = new Intent(InstrumentationRegistry.getInstrumentation().getContext(), TrackRecordingService.class);
InstrumentationRegistry.getInstrumentation().getContext().stopService(stopIntent); InstrumentationRegistry.getInstrumentation().getContext().stopService(stopIntent);
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME)); Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
BootReceiver bootReceiver = new BootReceiver(); BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_BOOT_COMPLETED); intent.setAction(Intent.ACTION_BOOT_COMPLETED);
bootReceiver.onReceive(InstrumentationRegistry.getInstrumentation().getContext(), intent); bootReceiver.onReceive(InstrumentationRegistry.getInstrumentation().getContext(), intent);
// Check if the service is started // Check if the service is started
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME)); Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
} }
/** /**
* Tests the behavior when receive notification which is not the phone boot. * Tests the behavior when receive notification which is not the phone boot.
*/ */
@Test @Test
public void testOnReceive_noStartService() { public void testOnReceive_noStartService() {
// Make sure no TrackRecordingService // Make sure no TrackRecordingService
Intent stopIntent = new Intent(InstrumentationRegistry.getInstrumentation().getContext(), TrackRecordingService.class); Intent stopIntent = new Intent(InstrumentationRegistry.getInstrumentation().getContext(), TrackRecordingService.class);
InstrumentationRegistry.getInstrumentation().getContext().stopService(stopIntent); InstrumentationRegistry.getInstrumentation().getContext().stopService(stopIntent);
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME)); Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
BootReceiver bootReceiver = new BootReceiver(); BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_BUG_REPORT); intent.setAction(Intent.ACTION_BUG_REPORT);
bootReceiver.onReceive(InstrumentationRegistry.getInstrumentation().getContext(), intent); bootReceiver.onReceive(InstrumentationRegistry.getInstrumentation().getContext(), intent);
// Check if the service is not started // Check if the service is not started
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME)); Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
} }
/** /**
* Checks if a service is started in a context. * Checks if a service is started in a context.
* *
* @param context the context for checking a service * @param context the context for checking a service
* @param serviceName the service name to find if existed * @param serviceName the service name to find if existed
*/ */
private boolean isServiceExisted(Context context, String serviceName) { private boolean isServiceExisted(Context context, String serviceName) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE); List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE);
for (int i = 0; i < serviceList.size(); i++) { for (int i = 0; i < serviceList.size(); i++) {
RunningServiceInfo serviceInfo = serviceList.get(i); RunningServiceInfo serviceInfo = serviceList.get(i);
ComponentName componentName = serviceInfo.service; ComponentName componentName = serviceInfo.service;
if (componentName.getClassName().equals(serviceName)) { if (componentName.getClassName().equals(serviceName)) {
return true; return true;
} }
}
return false;
} }
return false;
}
} }
@@ -31,57 +31,57 @@ import org.junit.runner.RunWith;
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ChartValueSeriesTest { public class ChartValueSeriesTest {
private ChartValueSeries series; private ChartValueSeries series;
@Before @Before
protected void setUp() { protected void setUp() {
series = new ChartValueSeries( series = new ChartValueSeries(
InstrumentationRegistry.getInstrumentation().getContext(), InstrumentationRegistry.getInstrumentation().getContext(),
Integer.MIN_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE, Integer.MAX_VALUE,
new int[] {100, 1000 }, new int[]{100, 1000},
R.string.description_elevation_metric, R.string.description_elevation_metric,
R.string.description_elevation_imperial, R.string.description_elevation_imperial,
R.color.chart_elevation_fill, R.color.chart_elevation_fill,
R.color.chart_elevation_border); R.color.chart_elevation_border);
} }
public void testInitialConditions() { public void testInitialConditions() {
Assert.assertEquals(1, series.getInterval()); Assert.assertEquals(1, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue()); Assert.assertEquals(0, series.getMinMarkerValue());
Assert.assertEquals(5, series.getMaxMarkerValue()); Assert.assertEquals(5, series.getMaxMarkerValue());
Assert.assertTrue(series.isEnabled()); Assert.assertTrue(series.isEnabled());
} }
public void testEnabled() { public void testEnabled() {
series.setEnabled(false); series.setEnabled(false);
Assert.assertFalse(series.isEnabled()); Assert.assertFalse(series.isEnabled());
} }
public void testSmallUpdates() { public void testSmallUpdates() {
series.update(0); series.update(0);
series.update(10); series.update(10);
series.updateDimension(); series.updateDimension();
Assert.assertEquals(100, series.getInterval()); Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue()); Assert.assertEquals(0, series.getMinMarkerValue());
Assert.assertEquals(500, series.getMaxMarkerValue()); Assert.assertEquals(500, series.getMaxMarkerValue());
} }
public void testBigUpdates() { public void testBigUpdates() {
series.update(0); series.update(0);
series.update(901); series.update(901);
series.updateDimension(); series.updateDimension();
Assert.assertEquals(1000, series.getInterval()); Assert.assertEquals(1000, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue()); Assert.assertEquals(0, series.getMinMarkerValue());
Assert.assertEquals(5000, series.getMaxMarkerValue()); Assert.assertEquals(5000, series.getMaxMarkerValue());
} }
public void testNotZeroBasedUpdates() { public void testNotZeroBasedUpdates() {
series.update(220); series.update(220);
series.update(250); series.update(250);
series.updateDimension(); series.updateDimension();
Assert.assertEquals(100, series.getInterval()); Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(200, series.getMinMarkerValue()); Assert.assertEquals(200, series.getMinMarkerValue());
Assert.assertEquals(700, series.getMaxMarkerValue()); Assert.assertEquals(700, series.getMaxMarkerValue());
} }
} }
@@ -20,7 +20,6 @@ import android.location.Location;
import com.google.android.apps.mytracks.content.SensorDataSetLocation; import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.sensor.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.sensor.SensorDataSet; import com.google.android.apps.mytracks.content.sensor.SensorDataSet;
/** /**
@@ -31,61 +30,61 @@ import com.google.android.apps.mytracks.content.sensor.SensorDataSet;
*/ */
public class TrackStubUtils { public class TrackStubUtils {
static final String LOCATION_PROVIDER = "gps"; public static final double INITIAL_ALTITUDE = 22;
static final double INITIAL_LATITUDE = 22; public static final long INITIAL_TIME = 1000L;
static final double INITIAL_LONGITUDE = 22; static final String LOCATION_PROVIDER = "gps";
public static final double INITIAL_ALTITUDE = 22; static final double INITIAL_LATITUDE = 22;
static final float INITIAL_ACCURACY = 5; static final double INITIAL_LONGITUDE = 22;
static final float INITIAL_SPEED = 10; static final float INITIAL_ACCURACY = 5;
public static final long INITIAL_TIME = 1000L; static final float INITIAL_SPEED = 10;
static final float INITIAL_BEARING = 3.0f; static final float INITIAL_BEARING = 3.0f;
// Used to change the value of latitude, longitude, and altitude. // Used to change the value of latitude, longitude, and altitude.
static final double DIFFERENCE = 0.01; static final double DIFFERENCE = 0.01;
/** /**
* Gets a a {@link Track} stub with specified number of locations. * Gets a a {@link Track} stub with specified number of locations.
* *
* @param numberOfLocations the number of locations for the track * @param numberOfLocations the number of locations for the track
* @return a track stub. * @return a track stub.
*/ */
public static Track createTrack(int numberOfLocations) { public static Track createTrack(int numberOfLocations) {
Track track = new Track(); Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) { for (int i = 0; i < numberOfLocations; i++) {
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE
+ i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE)); + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
}
return track;
} }
return track; /**
} * Create a MyTracks location with default values.
*
* @return a track stub.
*/
public static SensorDataSetLocation createSensorDataSetLocation() {
return createSensorDataSetLocation(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
}
/** /**
* Create a MyTracks location with default values. * Creates a {@link SensorDataSetLocation} stub with specified values.
* *
* @return a track stub. * @return a SensorDataSetLocation stub.
*/ */
public static SensorDataSetLocation createSensorDataSetLocation() { public static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude,
return createSensorDataSetLocation(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE); double altitude) {
} // Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
loc.setLongitude(longitude);
loc.setAltitude(altitude);
loc.setAccuracy(INITIAL_ACCURACY);
loc.setSpeed(INITIAL_SPEED);
loc.setTime(INITIAL_TIME);
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(100, 100);
/** return new SensorDataSetLocation(loc, sd);
* Creates a {@link SensorDataSetLocation} stub with specified values. }
*
* @return a SensorDataSetLocation stub.
*/
public static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude,
double altitude) {
// Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
loc.setLongitude(longitude);
loc.setAltitude(altitude);
loc.setAccuracy(INITIAL_ACCURACY);
loc.setSpeed(INITIAL_SPEED);
loc.setTime(INITIAL_TIME);
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(100, 100);
return new SensorDataSetLocation(loc, sd);
}
} }
@@ -36,108 +36,108 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class CustomContentProviderTest { public class CustomContentProviderTest {
private static final String DATABASE_NAME = "test.db"; private static final String DATABASE_NAME = "test.db";
private SQLiteDatabase db; private SQLiteDatabase db;
private CustomContentProvider customContentProvider; private CustomContentProvider customContentProvider;
@Before @Before
protected void setUp() { protected void setUp() {
InstrumentationRegistry.getInstrumentation().getContext().deleteDatabase(DATABASE_NAME); InstrumentationRegistry.getInstrumentation().getContext().deleteDatabase(DATABASE_NAME);
db = (new DatabaseHelper(InstrumentationRegistry.getInstrumentation().getContext(), DATABASE_NAME)).getWritableDatabase(); db = (new DatabaseHelper(InstrumentationRegistry.getInstrumentation().getContext(), DATABASE_NAME)).getWritableDatabase();
customContentProvider = new CustomContentProvider(); customContentProvider = new CustomContentProvider();
}
/**
* Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
*/
public void testDatabaseHelper_OnCreate() {
Assert.assertTrue(hasTable(TracksColumns.TABLE_NAME));
Assert.assertTrue(hasTable(TrackPointsColumns.TABLE_NAME));
Assert.assertTrue(hasTable(WaypointsColumns.TABLE_NAME));
}
/**
* Tests {@link CustomContentProvider#onCreate(android.content.Context)}.
*/
public void testOnCreate() {
Assert.assertTrue(customContentProvider.onCreate(InstrumentationRegistry.getInstrumentation().getContext()));
}
/**
* Tests {@link CustomContentProvider#getType(Uri)}.
*/
public void testGetType() {
Assert.assertEquals(TracksColumns.CONTENT_TYPE, customContentProvider.getType(TracksColumns.CONTENT_URI));
Assert.assertEquals(
TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI));
Assert.assertEquals(
WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
}
/**
* Creates a table, containing one column.
*
* @param table the table name
*/
private void createTable(String table) {
db.execSQL("CREATE TABLE " + table + " (test INTEGER)");
}
/**
* Drops a table in database.
*
* @param table the table name
*/
private void dropTable(String table) {
db.execSQL("Drop TABLE " + table);
}
/**
* Returns true if the table exists.
*
* @param table the table name
*/
private boolean hasTable(String table) {
try {
db.rawQuery("select count(*) from " + table, null);
return true;
} catch (Exception e) {
return false;
} }
}
/** /**
* Returns true if the column in the table exists. * Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
* */
* @param table the table name public void testDatabaseHelper_OnCreate() {
* @param column the column name Assert.assertTrue(hasTable(TracksColumns.TABLE_NAME));
*/ Assert.assertTrue(hasTable(TrackPointsColumns.TABLE_NAME));
private boolean hasColumn(String table, String column) { Assert.assertTrue(hasTable(WaypointsColumns.TABLE_NAME));
try {
db.execSQL("SElECT count(*) from " + table + " order by " + column);
} catch (Exception e) {
if (e.getMessage().indexOf("no such column") > -1) {
return false;
}
} }
return true;
}
/** /**
* Sets up upgrade. * Tests {@link CustomContentProvider#onCreate(android.content.Context)}.
* */
* @param oldVersion thd old database version public void testOnCreate() {
*/ Assert.assertTrue(customContentProvider.onCreate(InstrumentationRegistry.getInstrumentation().getContext()));
private void setupUpgrade(int oldVersion) { }
dropTable(TracksColumns.TABLE_NAME);
dropTable(TrackPointsColumns.TABLE_NAME);
dropTable(WaypointsColumns.TABLE_NAME);
createTable(TracksColumns.TABLE_NAME);
createTable(TrackPointsColumns.TABLE_NAME);
createTable(WaypointsColumns.TABLE_NAME);
DatabaseHelper databaseHelper = new DatabaseHelper(InstrumentationRegistry.getInstrumentation().getContext()); /**
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION); * Tests {@link CustomContentProvider#getType(Uri)}.
} */
public void testGetType() {
Assert.assertEquals(TracksColumns.CONTENT_TYPE, customContentProvider.getType(TracksColumns.CONTENT_URI));
Assert.assertEquals(
TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI));
Assert.assertEquals(
WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
}
/**
* Creates a table, containing one column.
*
* @param table the table name
*/
private void createTable(String table) {
db.execSQL("CREATE TABLE " + table + " (test INTEGER)");
}
/**
* Drops a table in database.
*
* @param table the table name
*/
private void dropTable(String table) {
db.execSQL("Drop TABLE " + table);
}
/**
* Returns true if the table exists.
*
* @param table the table name
*/
private boolean hasTable(String table) {
try {
db.rawQuery("select count(*) from " + table, null);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Returns true if the column in the table exists.
*
* @param table the table name
* @param column the column name
*/
private boolean hasColumn(String table, String column) {
try {
db.execSQL("SElECT count(*) from " + table + " order by " + column);
} catch (Exception e) {
if (e.getMessage().indexOf("no such column") > -1) {
return false;
}
}
return true;
}
/**
* Sets up upgrade.
*
* @param oldVersion thd old database version
*/
private void setupUpgrade(int oldVersion) {
dropTable(TracksColumns.TABLE_NAME);
dropTable(TrackPointsColumns.TABLE_NAME);
dropTable(WaypointsColumns.TABLE_NAME);
createTable(TracksColumns.TABLE_NAME);
createTable(TrackPointsColumns.TABLE_NAME);
createTable(WaypointsColumns.TABLE_NAME);
DatabaseHelper databaseHelper = new DatabaseHelper(InstrumentationRegistry.getInstrumentation().getContext());
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION);
}
} }
@@ -35,170 +35,170 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DescriptionGeneratorImplTest { public class DescriptionGeneratorImplTest {
private static final long START_TIME = 1288721514000L; private static final long START_TIME = 1288721514000L;
private DescriptionGeneratorImpl descriptionGenerator; private DescriptionGeneratorImpl descriptionGenerator;
@Before @Before
protected void setUp() throws Exception { protected void setUp() throws Exception {
descriptionGenerator = new DescriptionGeneratorImpl(InstrumentationRegistry.getInstrumentation().getContext()); descriptionGenerator = new DescriptionGeneratorImpl(InstrumentationRegistry.getInstrumentation().getContext());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, * Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track,
* java.util.Vector, java.util.Vector, boolean)}. * java.util.Vector, java.util.Vector, boolean)}.
*/ */
public void testGenerateTrackDescription() { public void testGenerateTrackDescription() {
Track track = new Track(); Track track = new Track();
TripStatistics stats = new TripStatistics(); TripStatistics stats = new TripStatistics();
stats.setTotalDistance(20000); stats.setTotalDistance(20000);
stats.setTotalTime(600000); stats.setTotalTime(600000);
stats.setMovingTime(300000); stats.setMovingTime(300000);
stats.setMaxSpeed(100); stats.setMaxSpeed(100);
stats.setMaxElevation(550); stats.setMaxElevation(550);
stats.setMinElevation(-500); stats.setMinElevation(-500);
stats.setTotalElevationGain(6000); stats.setTotalElevationGain(6000);
stats.setMaxGrade(0.42); stats.setMaxGrade(0.42);
stats.setMinGrade(0.11); stats.setMinGrade(0.11);
stats.setStartTime(START_TIME); stats.setStartTime(START_TIME);
track.setTripStatistics(stats); track.setTripStatistics(stats);
track.setCategory("hiking"); track.setCategory("hiking");
String expected = "Created by" String expected = "Created by"
+ " <a href='TODO'>TODO</a> on Android<p>" + " <a href='TODO'>TODO</a> on Android<p>"
+ "Name: -<br>" + "Name: -<br>"
+ "Activity type: hiking<br>" + "Activity type: hiking<br>"
+ "Description: -<br>" + "Description: -<br>"
+ "Total distance: 20.00 km (12.4 mi)<br>" + "Total distance: 20.00 km (12.4 mi)<br>"
+ "Total time: 10:00<br>" + "Total time: 10:00<br>"
+ "Moving time: 05:00<br>" + "Moving time: 05:00<br>"
+ "Average speed: 120.00 km/h (74.6 mi/h)<br>" + "Average speed: 120.00 km/h (74.6 mi/h)<br>"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)<br>" + "Average moving speed: 240.00 km/h (149.1 mi/h)<br>"
+ "Max speed: 360.00 km/h (223.7 mi/h)<br>" + "Max speed: 360.00 km/h (223.7 mi/h)<br>"
+ "Average pace: 0:30 min/km (0:48 min/mi)<br>" + "Average pace: 0:30 min/km (0:48 min/mi)<br>"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>" + "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>" + "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Max elevation: 550 m (1804 ft)<br>" + "Max elevation: 550 m (1804 ft)<br>"
+ "Min elevation: -500 m (-1640 ft)<br>" + "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>" + "Elevation gain: 6000 m (19685 ft)<br>"
+ "Max grade: 42 %<br>" + "Max grade: 42 %<br>"
+ "Min grade: 11 %<br>" + "Min grade: 11 %<br>"
+ "Recorded: " + StringUtils.formatDateTime(InstrumentationRegistry.getInstrumentation().getContext(), START_TIME) + "<br>"; + "Recorded: " + StringUtils.formatDateTime(InstrumentationRegistry.getInstrumentation().getContext(), START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, null, null, true)); Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, null, null, true));
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#generateWaypointDescription(TripStatistics)}. * Tests {@link DescriptionGeneratorImpl#generateWaypointDescription(TripStatistics)}.
*/ */
public void testGenerateWaypointDescription() { public void testGenerateWaypointDescription() {
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint();
TripStatistics stats = new TripStatistics(); TripStatistics stats = new TripStatistics();
stats.setTotalDistance(20000); stats.setTotalDistance(20000);
stats.setTotalTime(600000); stats.setTotalTime(600000);
stats.setMovingTime(300000); stats.setMovingTime(300000);
stats.setMaxSpeed(100); stats.setMaxSpeed(100);
stats.setMaxElevation(550); stats.setMaxElevation(550);
stats.setMinElevation(-500); stats.setMinElevation(-500);
stats.setTotalElevationGain(6000); stats.setTotalElevationGain(6000);
stats.setMaxGrade(0.42); stats.setMaxGrade(0.42);
stats.setMinGrade(0.11); stats.setMinGrade(0.11);
stats.setStartTime(START_TIME); stats.setStartTime(START_TIME);
waypoint.setTripStatistics(stats); waypoint.setTripStatistics(stats);
String expected = "Total distance: 20.00 km (12.4 mi)\n" String expected = "Total distance: 20.00 km (12.4 mi)\n"
+ "Total time: 10:00\n" + "Total time: 10:00\n"
+ "Moving time: 05:00\n" + "Moving time: 05:00\n"
+ "Average speed: 120.00 km/h (74.6 mi/h)\n" + "Average speed: 120.00 km/h (74.6 mi/h)\n"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)\n" + "Average moving speed: 240.00 km/h (149.1 mi/h)\n"
+ "Max speed: 360.00 km/h (223.7 mi/h)\n" + "Max speed: 360.00 km/h (223.7 mi/h)\n"
+ "Average pace: 0:30 min/km (0:48 min/mi)\n" + "Average pace: 0:30 min/km (0:48 min/mi)\n"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)\n" + "Average moving pace: 0:15 min/km (0:24 min/mi)\n"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)\n" + "Fastest pace: 0:10 min/km (0:16 min/mi)\n"
+ "Max elevation: 550 m (1804 ft)\n" + "Max elevation: 550 m (1804 ft)\n"
+ "Min elevation: -500 m (-1640 ft)\n" + "Min elevation: -500 m (-1640 ft)\n"
+ "Elevation gain: 6000 m (19685 ft)\n" + "Elevation gain: 6000 m (19685 ft)\n"
+ "Max grade: 42 %\n" + "Max grade: 42 %\n"
+ "Min grade: 11 %\n" + "Min grade: 11 %\n"
+ "Recorded: " + StringUtils.formatDateTime(InstrumentationRegistry.getInstrumentation().getContext(), START_TIME) + "\n"; + "Recorded: " + StringUtils.formatDateTime(InstrumentationRegistry.getInstrumentation().getContext(), START_TIME) + "\n";
Assert.assertEquals( Assert.assertEquals(
expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics())); expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics()));
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder,
* int, String)}. * int, String)}.
*/ */
public void testWriteDistance() { public void testWriteDistance() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>"); descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>");
Assert.assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString()); Assert.assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, * Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int,
* String)}. * String)}.
*/ */
public void testWriteTime() { public void testWriteTime() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeTime(1000, builder, R.string.description_total_time, "<br>"); descriptionGenerator.writeTime(1000, builder, R.string.description_total_time, "<br>");
Assert.assertEquals("Total time: 00:01<br>", builder.toString()); Assert.assertEquals("Total time: 00:01<br>", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder,
* int, String)}. * int, String)}.
*/ */
public void testWriteSpeed() { public void testWriteSpeed() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n"); descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n");
Assert.assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString()); Assert.assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder,
* int, String)}. * int, String)}.
*/ */
public void testWriteElevation() { public void testWriteElevation() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeElevation(4.2, builder, R.string.description_min_elevation, "<br>"); descriptionGenerator.writeElevation(4.2, builder, R.string.description_min_elevation, "<br>");
Assert.assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString()); Assert.assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, * Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int,
* String)}. * String)}.
*/ */
public void testWritePace() { public void testWritePace() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n"); descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
Assert.assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString()); Assert.assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder,
* int, String)}. * int, String)}.
*/ */
public void testWriteGrade() { public void testWriteGrade() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(.042, builder, R.string.description_max_grade, "<br>"); descriptionGenerator.writeGrade(.042, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 4 %<br>", builder.toString()); Assert.assertEquals("Max grade: 4 %<br>", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder,
* int, String)} with a NaN. * int, String)} with a NaN.
*/ */
public void testWriteGrade_nan() { public void testWriteGrade_nan() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(Double.NaN, builder, R.string.description_max_grade, "<br>"); descriptionGenerator.writeGrade(Double.NaN, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString()); Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder,
* int, String)} with an infinite number. * int, String)} with an infinite number.
*/ */
public void testWriteGrade_infinite() { public void testWriteGrade_infinite() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade( descriptionGenerator.writeGrade(
Double.POSITIVE_INFINITY, builder, R.string.description_max_grade, "<br>"); Double.POSITIVE_INFINITY, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString()); Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
} }
} }
@@ -48,281 +48,281 @@ import java.util.List;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SearchEngineTest { public class SearchEngineTest {
private static final Location HERE = new Location("gps"); private static final Location HERE = new Location("gps");
private static final long NOW = 1234567890000L; // After OLDEST_ALLOWED_TIMESTAMP private static final long NOW = 1234567890000L; // After OLDEST_ALLOWED_TIMESTAMP
private ContentProviderUtils providerUtils; private ContentProviderUtils providerUtils;
private SearchEngine engine; private SearchEngine engine;
@Before @Before
protected void setUp() { protected void setUp() {
MockContentResolver mockContentResolver = new MockContentResolver(); MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext( RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test."); getContext(), getContext(), "test.");
MockContext context = new MockContext(mockContentResolver, targetContext); MockContext context = new MockContext(mockContentResolver, targetContext);
CustomContentProvider provider = new CustomContentProvider(); CustomContentProvider provider = new CustomContentProvider();
provider.attachInfo(context, null); provider.attachInfo(context, null);
mockContentResolver.addProvider(ContentProviderUtils.AUTHORITY, provider); mockContentResolver.addProvider(ContentProviderUtils.AUTHORITY, provider);
setContext(context); setContext(context);
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = ContentProviderUtils.Factory.get(context);
engine = new SearchEngine(providerUtils); engine = new SearchEngine(providerUtils);
}
@After
protected void tearDown() throws Exception {
providerUtils.deleteAllTracks(getContext());
}
private long insertTrack(String title, String description, String category, double distance, long hoursAgo) {
Track track = new Track();
track.setName(title);
track.setDescription(description);
track.setCategory(category);
TripStatistics stats = track.getTripStatistics();
if (hoursAgo > 0) {
// Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime(NOW);
} }
int latitude = (int) ((HERE.getLatitude() + distance) * 1E6); @After
int longitude = (int) ((HERE.getLongitude() + distance) * 1E6); protected void tearDown() throws Exception {
stats.setBounds(latitude, longitude, latitude, longitude); providerUtils.deleteAllTracks(getContext());
Uri uri = providerUtils.insertTrack(track);
return ContentUris.parseId(uri);
}
private long insertTrack(String title, String description, String category) {
return insertTrack(title, description, category, 0, -1);
}
private long insertTrack(String title, double distance) {
return insertTrack(title, "", "", distance, -1);
}
private long insertTrack(String title, long hoursAgo) {
return insertTrack(title, "", "", 0.0, hoursAgo);
}
private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) {
Waypoint waypoint = new Waypoint();
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
} }
waypoint.setLocation(location);
Uri uri = providerUtils.insertWaypoint(waypoint); private long insertTrack(String title, String description, String category, double distance, long hoursAgo) {
return ContentUris.parseId(uri); Track track = new Track();
} track.setName(title);
track.setDescription(description);
track.setCategory(category);
private long insertWaypoint(String title, String description, String category) { TripStatistics stats = track.getTripStatistics();
return insertWaypoint(title, description, category, 0.0, -1, -1); if (hoursAgo > 0) {
} // Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime(NOW);
}
private long insertWaypoint(String title, double distance) { int latitude = (int) ((HERE.getLatitude() + distance) * 1E6);
return insertWaypoint(title, "", "", distance, -1, -1); int longitude = (int) ((HERE.getLongitude() + distance) * 1E6);
} stats.setBounds(latitude, longitude, latitude, longitude);
private long insertWaypoint(String title, long hoursAgo) { Uri uri = providerUtils.insertTrack(track);
return insertWaypoint(title, "", "", 0.0, hoursAgo, -1); return ContentUris.parseId(uri);
}
private long insertWaypoint(String title, long hoursAgo, long trackId) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId);
}
public void testSearchText() {
// Insert 7 tracks (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertTrack("bb", "cc", "dd");
long descriptionMatchId = insertTrack("bb", "aa", "cc");
long categoryMatchId = insertTrack("bb", "cc", "aa");
long titleMatchId = insertTrack("aa", "bb", "cc");
long titleCategoryMatchId = insertTrack("aa", "bb", "ca");
long titleDescriptionMatchId = insertTrack("aa", "ba", "cc");
long allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
}
public void testSearchWaypointText() {
// Insert 7 waypoints (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertWaypoint("bb", "cc", "dd");
long descriptionMatchId = insertWaypoint("bb", "aa", "cc");
long categoryMatchId = insertWaypoint("bb", "cc", "aa");
long titleMatchId = insertWaypoint("aa", "bb", "cc");
long titleCategoryMatchId = insertWaypoint("aa", "bb", "ca");
long titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc");
long allMatchId = insertWaypoint("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
}
public void testSearchMixedText() {
// Insert 5 entries (purposefully out of result order):
// - one waypoint which will match by description
// - one waypoint which won't match
// - one waypoint which will match by title
// - one track which won't match
// - one track which will match by title
long descriptionWaypointId = insertWaypoint("bb", "aa", "cc");
insertWaypoint("bb", "cc", "dd");
long titleWaypointId = insertWaypoint("aa", "bb", "cc");
insertTrack("bb", "cc", "dd");
long trackId = insertTrack("aa", "bb", "cc");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertEquals(results.toString(), 3, results.size());
assertTrackResult(trackId, results.get(0));
assertWaypointResult(titleWaypointId, results.get(1));
assertWaypointResult(descriptionWaypointId, results.get(2));
}
public void testSearchTrackDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertTrack("aa", 0.3);
long nearId = insertTrack("ab", 0.1);
long farId = insertTrack("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertTrackResults(results, nearId, farId, farFarAwayId);
}
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertWaypoint("aa", 0.3);
long nearId = insertWaypoint("ab", 0.1);
long farId = insertWaypoint("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
}
public void testSearchTrackRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertTrack("aa", 3);
long recentId = insertTrack("ab", 1);
long oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
}
public void testSearchWaypointRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertWaypoint("aa", 2);
long recentId = insertWaypoint("ab", 0);
long oldId = insertWaypoint("ac", 1);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
}
public void testSearchCurrentTrack() {
// All results match text, but one of them is the current track.
long currentId = insertTrack("ab", 1);
long otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
}
public void testSearchCurrentTrackWaypoint() {
// All results match text, but one of them is in the current track.
long otherId = insertWaypoint("aa", 1, 456);
long currentId = insertWaypoint("ab", 1, 123);
SearchQuery query = new SearchQuery("a", null, 123, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
}
private void assertTrackResult(long trackId, ScoredResult result) {
assertNotNull("Not a track", result.track);
assertNull("Ambiguous result", result.waypoint);
assertEquals(trackId, result.track.getId());
}
private void assertTrackResults(List<ScoredResult> results, long... trackIds) {
String errMsg = "Expected IDs=" + Arrays.toString(trackIds) + "; results=" + results;
assertEquals(results.size(), trackIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.track);
assertNull(errMsg, result.waypoint);
assertEquals(errMsg, trackIds[i], result.track.getId());
} }
}
private void assertWaypointResult(long waypointId, ScoredResult result) { private long insertTrack(String title, String description, String category) {
assertNotNull("Not a waypoint", result.waypoint); return insertTrack(title, description, category, 0, -1);
assertNull("Ambiguous result", result.track); }
assertEquals(waypointId, result.waypoint.getId());
} private long insertTrack(String title, double distance) {
return insertTrack(title, "", "", distance, -1);
private void assertWaypointResults(List<ScoredResult> results, long... waypointIds) { }
String errMsg = "Expected IDs=" + Arrays.toString(waypointIds) + "; results=" + results;
assertEquals(results.size(), waypointIds.length); private long insertTrack(String title, long hoursAgo) {
for (int i = 0; i < results.size(); i++) { return insertTrack(title, "", "", 0.0, hoursAgo);
ScoredResult result = results.get(i); }
assertNotNull(errMsg, result.waypoint);
assertNull(errMsg, result.track); private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) {
assertEquals(errMsg, waypointIds[i], result.waypoint.getId()); Waypoint waypoint = new Waypoint();
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
}
waypoint.setLocation(location);
Uri uri = providerUtils.insertWaypoint(waypoint);
return ContentUris.parseId(uri);
}
private long insertWaypoint(String title, String description, String category) {
return insertWaypoint(title, description, category, 0.0, -1, -1);
}
private long insertWaypoint(String title, double distance) {
return insertWaypoint(title, "", "", distance, -1, -1);
}
private long insertWaypoint(String title, long hoursAgo) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, -1);
}
private long insertWaypoint(String title, long hoursAgo, long trackId) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId);
}
public void testSearchText() {
// Insert 7 tracks (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertTrack("bb", "cc", "dd");
long descriptionMatchId = insertTrack("bb", "aa", "cc");
long categoryMatchId = insertTrack("bb", "cc", "aa");
long titleMatchId = insertTrack("aa", "bb", "cc");
long titleCategoryMatchId = insertTrack("aa", "bb", "ca");
long titleDescriptionMatchId = insertTrack("aa", "ba", "cc");
long allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
}
public void testSearchWaypointText() {
// Insert 7 waypoints (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertWaypoint("bb", "cc", "dd");
long descriptionMatchId = insertWaypoint("bb", "aa", "cc");
long categoryMatchId = insertWaypoint("bb", "cc", "aa");
long titleMatchId = insertWaypoint("aa", "bb", "cc");
long titleCategoryMatchId = insertWaypoint("aa", "bb", "ca");
long titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc");
long allMatchId = insertWaypoint("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
}
public void testSearchMixedText() {
// Insert 5 entries (purposefully out of result order):
// - one waypoint which will match by description
// - one waypoint which won't match
// - one waypoint which will match by title
// - one track which won't match
// - one track which will match by title
long descriptionWaypointId = insertWaypoint("bb", "aa", "cc");
insertWaypoint("bb", "cc", "dd");
long titleWaypointId = insertWaypoint("aa", "bb", "cc");
insertTrack("bb", "cc", "dd");
long trackId = insertTrack("aa", "bb", "cc");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertEquals(results.toString(), 3, results.size());
assertTrackResult(trackId, results.get(0));
assertWaypointResult(titleWaypointId, results.get(1));
assertWaypointResult(descriptionWaypointId, results.get(2));
}
public void testSearchTrackDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertTrack("aa", 0.3);
long nearId = insertTrack("ab", 0.1);
long farId = insertTrack("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertTrackResults(results, nearId, farId, farFarAwayId);
}
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertWaypoint("aa", 0.3);
long nearId = insertWaypoint("ab", 0.1);
long farId = insertWaypoint("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
}
public void testSearchTrackRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertTrack("aa", 3);
long recentId = insertTrack("ab", 1);
long oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
}
public void testSearchWaypointRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertWaypoint("aa", 2);
long recentId = insertWaypoint("ab", 0);
long oldId = insertWaypoint("ac", 1);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
}
public void testSearchCurrentTrack() {
// All results match text, but one of them is the current track.
long currentId = insertTrack("ab", 1);
long otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
}
public void testSearchCurrentTrackWaypoint() {
// All results match text, but one of them is in the current track.
long otherId = insertWaypoint("aa", 1, 456);
long currentId = insertWaypoint("ab", 1, 123);
SearchQuery query = new SearchQuery("a", null, 123, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
}
private void assertTrackResult(long trackId, ScoredResult result) {
assertNotNull("Not a track", result.track);
assertNull("Ambiguous result", result.waypoint);
assertEquals(trackId, result.track.getId());
}
private void assertTrackResults(List<ScoredResult> results, long... trackIds) {
String errMsg = "Expected IDs=" + Arrays.toString(trackIds) + "; results=" + results;
assertEquals(results.size(), trackIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.track);
assertNull(errMsg, result.waypoint);
assertEquals(errMsg, trackIds[i], result.track.getId());
}
}
private void assertWaypointResult(long waypointId, ScoredResult result) {
assertNotNull("Not a waypoint", result.waypoint);
assertNull("Ambiguous result", result.track);
assertEquals(waypointId, result.waypoint.getId());
}
private void assertWaypointResults(List<ScoredResult> results, long... waypointIds) {
String errMsg = "Expected IDs=" + Arrays.toString(waypointIds) + "; results=" + results;
assertEquals(results.size(), waypointIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.waypoint);
assertNull(errMsg, result.track);
assertEquals(errMsg, waypointIds[i], result.waypoint.getId());
}
} }
}
} }
File diff suppressed because it is too large Load Diff
@@ -33,31 +33,31 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class WaypointCreationRequestTest { public class WaypointCreationRequestTest {
public void testTypeParceling() { public void testTypeParceling() {
WaypointCreationRequest original = WaypointCreationRequest.DEFAULT_WAYPOINT; WaypointCreationRequest original = WaypointCreationRequest.DEFAULT_WAYPOINT;
Parcel p = Parcel.obtain(); Parcel p = Parcel.obtain();
original.writeToParcel(p, 0); original.writeToParcel(p, 0);
p.setDataPosition(0); p.setDataPosition(0);
WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p); WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p);
Assert.assertEquals(original.getType(), copy.getType()); Assert.assertEquals(original.getType(), copy.getType());
Assert.assertFalse(copy.isTrackStatistics()); Assert.assertFalse(copy.isTrackStatistics());
Assert.assertNull(copy.getName()); Assert.assertNull(copy.getName());
Assert.assertNull(copy.getDescription()); Assert.assertNull(copy.getDescription());
Assert.assertNull(copy.getIconUrl()); Assert.assertNull(copy.getIconUrl());
} }
public void testAllAttributesParceling() { public void testAllAttributesParceling() {
WaypointCreationRequest original = new WaypointCreationRequest( WaypointCreationRequest original = new WaypointCreationRequest(
WaypointType.WAYPOINT, false, "name", "category", "description", "img.png", null); WaypointType.WAYPOINT, false, "name", "category", "description", "img.png", null);
Parcel p = Parcel.obtain(); Parcel p = Parcel.obtain();
original.writeToParcel(p, 0); original.writeToParcel(p, 0);
p.setDataPosition(0); p.setDataPosition(0);
WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p); WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p);
Assert.assertEquals(original.getType(), copy.getType()); Assert.assertEquals(original.getType(), copy.getType());
Assert.assertFalse(copy.isTrackStatistics()); Assert.assertFalse(copy.isTrackStatistics());
Assert.assertEquals("name", copy.getName()); Assert.assertEquals("name", copy.getName());
Assert.assertEquals("category", copy.getCategory()); Assert.assertEquals("category", copy.getCategory());
Assert.assertEquals("description", copy.getDescription()); Assert.assertEquals("description", copy.getDescription());
Assert.assertEquals("img.png", copy.getIconUrl()); Assert.assertEquals("img.png", copy.getIconUrl());
} }
} }
@@ -40,318 +40,318 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ChartFragmentTest { public class ChartFragmentTest {
private static final double HOURS_PER_UNIT = 60.0; private static final double HOURS_PER_UNIT = 60.0;
private ChartFragment chartFragment; private ChartFragment chartFragment;
@Before @Before
protected void setUp() { protected void setUp() {
chartFragment = new ChartFragment(); chartFragment = new ChartFragment();
chartFragment.setChartView(new ChartView(InstrumentationRegistry.getInstrumentation().getContext())); chartFragment.setChartView(new ChartView(InstrumentationRegistry.getInstrumentation().getContext()));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME); chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
} }
/** /**
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location, * Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location,
* double[])} * double[])}
*/
public void testFillDataPoint_sensorIncorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
// Input incorrect state.
// Creates SensorData.
Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder()
.setValue(100).setState(Sensor.SensorState.NONE);
Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder()
.setValue(101).setState(Sensor.SensorState.NONE);
Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder()
.setValue(102).setState(Sensor.SensorState.NONE);
// Creates SensorDataSet.
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
sensorDataSet = sensorDataSet.toBuilder()
.setHeartRate(heartRateData).setCadence(cadenceData).setPower(powerData).build();
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
}
/**
* Tests the logic to get the correct values of sensor in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
*/
public void testFillDataPoint_sensorCorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
// Creates SensorData.
Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder()
.setValue(100).setState(Sensor.SensorState.SENDING);
Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder()
.setValue(101).setState(Sensor.SensorState.SENDING);
Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder()
.setValue(102).setState(Sensor.SensorState.SENDING);
// Creates SensorDataSet.
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
sensorDataSet = sensorDataSet.toBuilder()
.setHeartRate(heartRateData).setCadence(cadenceData).setPower(powerData).build();
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1]);
}
/**
* Tests the logic to get the value of metric Distance in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
*/
public void testFillDataPoint_distanceMetric() {
// By distance.
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
// The second is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0]);
// The third location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation3);
// Computes the distance between Latitude 22 and 23.
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals(distance1, point[0]);
// The fourth location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation4.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation4);
// Computes the distance between Latitude 23 and 24.
Location.distanceBetween(sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(),
sensorDataSetLocation4.getLatitude(), sensorDataSetLocation4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals((distance1 + distance2), point[0]);
}
/**
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
*/
public void testFillDataPoint_distanceImperial() {
// By distance.
chartFragment.setChartByDistance(true);
// Setups to use imperial.
chartFragment.setMetricUnits(false);
// The first is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
// The second location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation2);
/*
* Computes the distance between Latitude 22 and 23. And for we set using
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/ */
float[] results = new float[4]; public void testFillDataPoint_sensorIncorrect() {
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0]);
// The third location is a new location, and use imperial. // No input.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation(); double[] point = fillDataPointTestHelper(sensorDataSetLocation);
sensorDataSetLocation3.setLatitude(24); Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
point = fillDataPointTestHelper(sensorDataSetLocation3); Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
/* // Input incorrect state.
* Computes the distance between Latitude 23 and 24. And for we set using // Creates SensorData.
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI. Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder()
.setValue(100).setState(Sensor.SensorState.NONE);
Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder()
.setValue(101).setState(Sensor.SensorState.NONE);
Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder()
.setValue(102).setState(Sensor.SensorState.NONE);
// Creates SensorDataSet.
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
sensorDataSet = sensorDataSet.toBuilder()
.setHeartRate(heartRateData).setCadence(cadenceData).setPower(powerData).build();
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
}
/**
* Tests the logic to get the correct values of sensor in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
*/ */
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), public void testFillDataPoint_sensorCorrect() {
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results); SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; // No input.
Assert.assertEquals(distance1 + distance2, point[0]); double[] point = fillDataPointTestHelper(sensorDataSetLocation);
} Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
/** // Creates SensorData.
* Tests the logic to get the values of time in {@link ChartFragment#fillDataPoint(Location, Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder()
* double[])}. .setValue(100).setState(Sensor.SensorState.SENDING);
*/ Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder()
public void testFillDataPoint_time() { .setValue(101).setState(Sensor.SensorState.SENDING);
// By time Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder()
chartFragment.setChartByDistance(false); .setValue(102).setState(Sensor.SensorState.SENDING);
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
long timeSpan = 222;
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0]);
}
/** // Creates SensorDataSet.
* Tests the logic to get the value of elevation in {@link ChartFragment#fillDataPoint(Location, SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
* double[])} by one and two points. sensorDataSet = sensorDataSet.toBuilder()
*/ .setHeartRate(heartRateData).setCadence(cadenceData).setPower(powerData).build();
public void testFillDataPoint_elevation() { sensorDataSetLocation.setSensorDataSet(sensorDataSet);
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); // Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1]);
}
/* /**
* At first, clear old points of elevation, so give true to the second * Tests the logic to get the value of metric Distance in {@link ChartFragment#fillDataPoint(Location,
* parameter. Then only one value INITIALLONGTITUDE in buffer. * double[])}.
*/ */
double[] point = fillDataPointTestHelper(sensorDataSetLocation1); public void testFillDataPoint_distanceMetric() {
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1]); // By distance.
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
/* // The second is a same location, just different time.
* Send another value to buffer, now there are two values, INITIALALTITUDE SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
* and INITIALALTITUDE * 2. point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0]);
// The third location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation3);
// Computes the distance between Latitude 22 and 23.
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals(distance1, point[0]);
// The fourth location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation4.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation4);
// Computes the distance between Latitude 23 and 24.
Location.distanceBetween(sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(),
sensorDataSetLocation4.getLatitude(), sensorDataSetLocation4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals((distance1 + distance2), point[0]);
}
/**
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
*/ */
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); public void testFillDataPoint_distanceImperial() {
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2); // By distance.
point = fillDataPointTestHelper(sensorDataSetLocation2); chartFragment.setChartByDistance(true);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, // Setups to use imperial.
point[ChartView.ELEVATION_SERIES + 1]); chartFragment.setMetricUnits(false);
}
/** // The first is a same location, just different time.
* Tests the logic to get the value of speed in {@link ChartFragment#fillDataPoint(Location, SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
* double[])}. In this test, firstly remove all double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
* points in memory, and then fill in two points one by one. The speed values Assert.assertEquals(0.0, point[0]);
* of these points are 129, 130.
*/ // The second location is a new location, and use imperial.
public void testFillDataPoint_speed() { SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* sensorDataSetLocation2.setLatitude(23);
* At first, clear old points of speed, so give true to the second point = fillDataPointTestHelper(sensorDataSetLocation2);
* parameter. It will not be filled in to the speed buffer.
/*
* Computes the distance between Latitude 22 and 23. And for we set using
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(),
sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0]);
// The third location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation3);
/*
* Computes the distance between Latitude 23 and 24. And for we set using
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1 + distance2, point[0]);
}
/**
* Tests the logic to get the values of time in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
*/ */
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); public void testFillDataPoint_time() {
sensorDataSetLocation1.setSpeed(128.5f); // By time
double[] point = fillDataPointTestHelper(sensorDataSetLocation1); chartFragment.setChartByDistance(false);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]); SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
long timeSpan = 222;
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0]);
}
/* /**
* Tests the logic when both metricUnits and reportSpeed are true.This * Tests the logic to get the value of elevation in {@link ChartFragment#fillDataPoint(Location,
* location will be filled into speed buffer. * double[])} by one and two points.
*/ */
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); public void testFillDataPoint_elevation() {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid, the value * At first, clear old points of elevation, so give true to the second
* 222 here is doesn't matter. * parameter. Then only one value INITIALLONGTITUDE in buffer.
*/
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1]);
/*
* Send another value to buffer, now there are two values, INITIALALTITUDE
* and INITIALALTITUDE * 2.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0,
point[ChartView.ELEVATION_SERIES + 1]);
}
/**
* Tests the logic to get the value of speed in {@link ChartFragment#fillDataPoint(Location,
* double[])}. In this test, firstly remove all
* points in memory, and then fill in two points one by one. The speed values
* of these points are 129, 130.
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222); public void testFillDataPoint_speed() {
sensorDataSetLocation2.setSpeed(130); /*
point = fillDataPointTestHelper(sensorDataSetLocation2); * At first, clear old points of speed, so give true to the second
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1]); * parameter. It will not be filled in to the speed buffer.
} */
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
/** /*
* Tests the logic to compute speed when use Imperial. * Tests the logic when both metricUnits and reportSpeed are true.This
*/ * location will be filled into speed buffer.
public void testFillDataPoint_speedImperial() { */
// Setups to use imperial. SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
chartFragment.setMetricUnits(false);
// First data point is not added to the speed buffer /*
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); * Add a time span here to make sure the second point is valid, the value
sensorDataSetLocation1.setSpeed(100.0f); * 222 here is doesn't matter.
double[] point = fillDataPointTestHelper(sensorDataSetLocation1); */
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]); sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222);
sensorDataSetLocation2.setSpeed(130);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1]);
}
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); /**
* Tests the logic to compute speed when use Imperial.
/*
* Add a time span here to make sure the second point is valid and the speed
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222); public void testFillDataPoint_speedImperial() {
sensorDataSetLocation2.setSpeed(102); // Setups to use imperial.
point = fillDataPointTestHelper(sensorDataSetLocation2); chartFragment.setMetricUnits(false);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1]);
}
/** // First data point is not added to the speed buffer
* Tests the logic to get pace value when reportSpeed is false. SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
*/ sensorDataSetLocation1.setSpeed(100.0f);
public void testFillDataPoint_pace_nonZeroSpeed() { double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
// Setups reportSpeed to false. Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
chartFragment.setReportSpeed(false);
// First data point is not added to the speed buffer SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); /*
* Add a time span here to make sure the second point is valid and the speed
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1]);
}
/* /**
* Add a time span here to make sure the second point is valid and the speed * Tests the logic to get pace value when reportSpeed is false.
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222); public void testFillDataPoint_pace_nonZeroSpeed() {
sensorDataSetLocation2.setSpeed(102); // Setups reportSpeed to false.
point = fillDataPointTestHelper(sensorDataSetLocation2); chartFragment.setReportSpeed(false);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1]);
}
/** // First data point is not added to the speed buffer
* Tests the logic to get pace value when reportSpeed is false and average SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
* speed is zero. sensorDataSetLocation1.setSpeed(100.0f);
*/ double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
public void testFillDataPoint_pace_zeroSpeed() { Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
// Setups reportSpeed to false.
chartFragment.setReportSpeed(false);
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1]);
}
/** SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
* Helper method to test fillDataPoint.
* /*
* @param location location to fill * Add a time span here to make sure the second point is valid and the speed
* @return data of this location * is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION
*/ * * timeDifference speedDifference = 102 -100 timeDifference = 222
private double[] fillDataPointTestHelper(Location location) { */
double[] point = new double[ChartView.NUM_SERIES + 1]; sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
chartFragment.fillDataPoint(location, point); sensorDataSetLocation2.setSpeed(102);
return point; point = fillDataPointTestHelper(sensorDataSetLocation2);
} Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1]);
}
/**
* Tests the logic to get pace value when reportSpeed is false and average
* speed is zero.
*/
public void testFillDataPoint_pace_zeroSpeed() {
// Setups reportSpeed to false.
chartFragment.setReportSpeed(false);
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1]);
}
/**
* Helper method to test fillDataPoint.
*
* @param location location to fill
* @return data of this location
*/
private double[] fillDataPointTestHelper(Location location) {
double[] point = new double[ChartView.NUM_SERIES + 1];
chartFragment.fillDataPoint(location, point);
return point;
}
} }
@@ -16,8 +16,11 @@
package com.google.android.apps.mytracks.io.file.importer; package com.google.android.apps.mytracks.io.file.importer;
import static com.google.android.testing.mocking.AndroidMock.eq; import android.content.ContentUris;
import static com.google.android.testing.mocking.AndroidMock.expect; import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.test.AndroidTestCase;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.ContentProviderUtils.Factory; import com.google.android.apps.mytracks.content.ContentProviderUtils.Factory;
@@ -29,17 +32,14 @@ import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks; import com.google.android.testing.mocking.UsesMocks;
import android.content.ContentUris; import org.easymock.Capture;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.test.AndroidTestCase;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Locale; import java.util.Locale;
import java.util.SimpleTimeZone; import java.util.SimpleTimeZone;
import org.easymock.Capture; import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
/** /**
* Abstract class for testing file track importers. * Abstract class for testing file track importers.
@@ -48,117 +48,115 @@ import org.easymock.Capture;
*/ */
public class AbstractTestFileTrackImporter extends AndroidTestCase { public class AbstractTestFileTrackImporter extends AndroidTestCase {
protected static final String TRACK_NAME_0 = "blablub"; protected static final String TRACK_NAME_0 = "blablub";
protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer"; protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
protected static final String TRACK_NAME_1 = "another track"; protected static final String TRACK_NAME_1 = "another track";
protected static final String TRACK_DESCRIPTION_1 = "another description"; protected static final String TRACK_DESCRIPTION_1 = "another description";
protected static final double TRACK_LATITUDE = 48.768364; protected static final double TRACK_LATITUDE = 48.768364;
protected static final double TRACK_LONGITUDE = 9.177886; protected static final double TRACK_LONGITUDE = 9.177886;
protected static final double TRACK_ELEVATION = 324.0; protected static final double TRACK_ELEVATION = 324.0;
protected static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z"; protected static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123"; protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123"; protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123"; protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat( protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat(
"yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US); "yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat( protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US); "yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
protected static final long TRACK_ID_0 = 1;
protected static final long TRACK_ID_1 = 2;
protected static final long TRACK_POINT_ID_0 = 1;
protected static final long TRACK_POINT_ID_1 = 2;
protected static final long TRACK_POINT_ID_3 = 4;
protected static final long WAYPOINT_ID_0 = 1;
protected static final Uri TRACK_ID_0_URI = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
protected static final Uri TRACK_ID_1_URI = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build();
protected static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(
WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
static { static {
/* /*
* We can't omit the timezones in the test, otherwise it'll use the local * We can't omit the timezones in the test, otherwise it'll use the local
* timezone and fail depending on where the test runner is. * timezone and fail depending on where the test runner is.
*/
SimpleTimeZone utc = new SimpleTimeZone(0, "UTC");
DATE_FORMAT_0.setTimeZone(utc);
DATE_FORMAT_1.setTimeZone(utc);
}
protected ContentProviderUtils contentProviderUtils;
private Factory oldMyTracksProviderUtilsFactory;
@UsesMocks(ContentProviderUtils.class)
@Override
protected void setUp() throws Exception {
super.setUp();
contentProviderUtils = AndroidMock.createMock(ContentProviderUtils.class);
oldMyTracksProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(
contentProviderUtils);
}
@Override
protected void tearDown() throws Exception {
TestingProviderUtilsFactory.restoreOldFactory(oldMyTracksProviderUtilsFactory);
super.tearDown();
}
protected Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index);
location.setAltitude(TRACK_ELEVATION + index);
location.setTime(time);
return location;
}
/**
* Expects the first track point to be added.
*
* @param location the location
* @param trackId the track id
* @param trackPointId the track point id
*/ */
SimpleTimeZone utc = new SimpleTimeZone(0, "UTC"); protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
DATE_FORMAT_0.setTimeZone(utc); expect(contentProviderUtils.bulkInsertTrackPoint(
DATE_FORMAT_1.setTimeZone(utc); location != null ? LocationsMatcher.eqLoc(location) : (Location[]) AndroidMock.anyObject(),
} eq(1), eq(trackId))).andReturn(1);
expect(contentProviderUtils.getFirstTrackPointId(trackId)).andReturn(trackPointId);
protected static final long TRACK_ID_0 = 1; expect(contentProviderUtils.getLastTrackPointId(trackId)).andReturn(trackPointId);
protected static final long TRACK_ID_1 = 2;
protected static final long TRACK_POINT_ID_0 = 1;
protected static final long TRACK_POINT_ID_1 = 2;
protected static final long TRACK_POINT_ID_3 = 4;
protected static final long WAYPOINT_ID_0 = 1;
protected static final Uri TRACK_ID_0_URI = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
protected static final Uri TRACK_ID_1_URI = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build();
protected static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(
WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected ContentProviderUtils contentProviderUtils;
private Factory oldMyTracksProviderUtilsFactory;
@UsesMocks(ContentProviderUtils.class)
@Override
protected void setUp() throws Exception {
super.setUp();
contentProviderUtils = AndroidMock.createMock(ContentProviderUtils.class);
oldMyTracksProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(
contentProviderUtils);
}
@Override
protected void tearDown() throws Exception {
TestingProviderUtilsFactory.restoreOldFactory(oldMyTracksProviderUtilsFactory);
super.tearDown();
}
protected Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index);
location.setAltitude(TRACK_ELEVATION + index);
location.setTime(time);
return location;
}
/**
* Expects the first track point to be added.
*
* @param location the location
* @param trackId the track id
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
expect(contentProviderUtils.bulkInsertTrackPoint(
location != null ? LocationsMatcher.eqLoc(location) : (Location[]) AndroidMock.anyObject(),
eq(1), eq(trackId))).andReturn(1);
expect(contentProviderUtils.getFirstTrackPointId(trackId)).andReturn(trackPointId);
expect(contentProviderUtils.getLastTrackPointId(trackId)).andReturn(trackPointId);
}
/**
* Expects the track to be updated.
*
* @param track the track
* @param lastTrack true if it is the last track in the gpx
* @param trackId the track id
*/
protected void expectUpdateTrack(Capture<Track> track, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(AndroidMock.capture(track));
expect(contentProviderUtils.insertWaypoint((Waypoint) AndroidMock.anyObject()))
.andReturn(WAYPOINT_ID_O_URI);
if (lastTrack) {
// Return null to not add waypoints
expect(contentProviderUtils.getTrack(trackId)).andReturn(null);
} }
}
protected void verifyTrack(Track track, String name, String description, long time) { /**
assertEquals(name, track.getName()); * Expects the track to be updated.
assertEquals(description, track.getDescription()); *
if (time != -1L) { * @param track the track
assertEquals(time, track.getTripStatistics().getStartTime()); * @param lastTrack true if it is the last track in the gpx
* @param trackId the track id
*/
protected void expectUpdateTrack(Capture<Track> track, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(AndroidMock.capture(track));
expect(contentProviderUtils.insertWaypoint((Waypoint) AndroidMock.anyObject()))
.andReturn(WAYPOINT_ID_O_URI);
if (lastTrack) {
// Return null to not add waypoints
expect(contentProviderUtils.getTrack(trackId)).andReturn(null);
}
}
protected void verifyTrack(Track track, String name, String description, long time) {
assertEquals(name, track.getName());
assertEquals(description, track.getDescription());
if (time != -1L) {
assertEquals(time, track.getTripStatistics().getStartTime());
}
assertNotSame(-1, track.getStartId());
assertNotSame(-1, track.getStopId());
} }
assertNotSame(-1, track.getStartId());
assertNotSame(-1, track.getStopId());
}
} }
@@ -16,20 +16,20 @@
package com.google.android.apps.mytracks.io.file.importer; package com.google.android.apps.mytracks.io.file.importer;
import static com.google.android.testing.mocking.AndroidMock.eq; import android.location.Location;
import static com.google.android.testing.mocking.AndroidMock.expect;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.AndroidMock;
import android.location.Location; import org.easymock.Capture;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import org.easymock.Capture; import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
/** /**
* Tests for {@link GpxFileTrackImporter}. * Tests for {@link GpxFileTrackImporter}.
@@ -38,220 +38,219 @@ import org.easymock.Capture;
*/ */
public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter { public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
private static String getNameAndDescription(String name, String description) { private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = "<gpx><trk>"
return "<name><![CDATA[" + name + "]]></name>" + "<desc><![CDATA[" + description + "]]></desc>"; + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>"
} + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</trkseg></trk></gpx>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = "<gpx><trk>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</trkseg><trkseg>"
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3) + "</trkseg></trk></gpx>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX = "<gpx><trk>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>"
+ getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>"
+ getTrackPoint(2, null) + getTrackPoint(3, null) + "</trkseg></trk></gpx>";
private static final String INVALID_XML_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.substring(
0, VALID_ONE_TRACK_ONE_SEGMENT_GPX.length() - 50);
private static final String INVALID_LOCATION_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LATITUDE), "1000.0");
private static final String INVALID_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
TRACK_TIME_0, "invalid");
private static final String INVALID_ALTITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_ELEVATION), "invalid");
private static final String INVALID_LATITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LATITUDE), "invalid");
private static final String INVALID_LONGITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LONGITUDE), "invalid");
private static String getTrackPoint(int index, String time) { private static String getNameAndDescription(String name, String description) {
String latitude = Double.toString(TRACK_LATITUDE + index); return "<name><![CDATA[" + name + "]]></name>" + "<desc><![CDATA[" + description + "]]></desc>";
String longitude = Double.toString(TRACK_LONGITUDE + index);
String elevation = Double.toString(TRACK_ELEVATION + index);
StringBuffer buffer = new StringBuffer();
buffer.append(
"<trkpt lat=\"" + latitude + "\" lon=\"" + longitude + "\"><ele>" + elevation + "</ele>");
if (time != null) {
buffer.append("<time>" + time + "</time>");
} }
buffer.append("</trkpt>");
return buffer.toString();
}
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = "<gpx><trk>" private static String getTrackPoint(int index, String time) {
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>" String latitude = Double.toString(TRACK_LATITUDE + index);
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</trkseg></trk></gpx>"; String longitude = Double.toString(TRACK_LONGITUDE + index);
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = "<gpx><trk>" String elevation = Double.toString(TRACK_ELEVATION + index);
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>" StringBuffer buffer = new StringBuffer();
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</trkseg><trkseg>" buffer.append(
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3) + "</trkseg></trk></gpx>"; "<trkpt lat=\"" + latitude + "\" lon=\"" + longitude + "\"><ele>" + elevation + "</ele>");
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX = "<gpx><trk>" if (time != null) {
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>" buffer.append("<time>" + time + "</time>");
+ getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>" }
+ getTrackPoint(2, null) + getTrackPoint(3, null) + "</trkseg></trk></gpx>"; buffer.append("</trkpt>");
return buffer.toString();
}
private static final String INVALID_XML_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.substring( /**
0, VALID_ONE_TRACK_ONE_SEGMENT_GPX.length() - 50); * Tests one track with one segment.
private static final String INVALID_LOCATION_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll( */
Double.toString(TRACK_LATITUDE), "1000.0"); public void testOneTrackOneSegment() throws Exception {
private static final String INVALID_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll( Capture<Track> track = new Capture<Track>();
TRACK_TIME_0, "invalid");
private static final String INVALID_ALTITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_ELEVATION), "invalid");
private static final String INVALID_LATITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LATITUDE), "invalid");
private static final String INVALID_LONGITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LONGITUDE), "invalid");
/** Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
* Tests one track with one segment. Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
*/
public void testOneTrackOneSegment() throws Exception {
Capture<Track> track = new Capture<Track>();
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); .andReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject())) // A flush happens at the end
.andReturn(TRACK_ID_0_URI); expect(contentProviderUtils.bulkInsertTrackPoint(
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
// A flush happens at the end InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
expect(contentProviderUtils.bulkInsertTrackPoint( GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1); getContext(), contentProviderUtils);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1); long trackId = gpxFileTrackImporter.importFile(inputStream);
expect( assertEquals(TRACK_ID_0, trackId);
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter( long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
getContext(), contentProviderUtils); assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime());
long trackId = gpxFileTrackImporter.importFile(inputStream); AndroidMock.verify(contentProviderUtils);
assertEquals(TRACK_ID_0, trackId); verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); /**
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); * Tests one track with two segments.
assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime()); */
AndroidMock.verify(contentProviderUtils); public void testOneTrackTwoSegments() throws Exception {
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); Capture<Track> track = new Capture<Track>();
}
/** Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
* Tests one track with two segments.
*/
public void testOneTrackTwoSegments() throws Exception {
Capture<Track> track = new Capture<Track>();
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
.andReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject())) InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
.andReturn(TRACK_ID_0_URI); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); getContext(), contentProviderUtils);
// A flush happens at the end long trackId = gpxFileTrackImporter.importFile(inputStream);
expect(contentProviderUtils.bulkInsertTrackPoint( assertEquals(TRACK_ID_0, trackId);
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter( long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
getContext(), contentProviderUtils); long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long trackId = gpxFileTrackImporter.importFile(inputStream); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
assertEquals(TRACK_ID_0, trackId); assertEquals(
time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime());
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); AndroidMock.verify(contentProviderUtils);
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0,
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime(); DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); }
assertEquals(
time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils); /**
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, * Tests one track with two segments, but no time in the track points.
DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); */
} public void testOneTrackTwoSegmentsNoTime() throws Exception {
Capture<Track> track = new Capture<Track>();
/** expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
* Tests one track with two segments, but no time in the track points. .andReturn(TRACK_ID_0_URI);
*/ expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
public void testOneTrackTwoSegmentsNoTime() throws Exception {
Capture<Track> track = new Capture<Track>();
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject())) // A flush happens at the end
.andReturn(TRACK_ID_0_URI); expect(contentProviderUtils.bulkInsertTrackPoint(
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0); (Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
// A flush happens at the end InputStream inputStream = new ByteArrayInputStream(
expect(contentProviderUtils.bulkInsertTrackPoint( VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes());
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3); getContext(), contentProviderUtils);
expect( long trackId = gpxFileTrackImporter.importFile(inputStream);
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(), assertEquals(TRACK_ID_0, trackId);
R.string.recording_track_id_key))).andStubReturn(null); assertEquals(0, track.getValue().getTripStatistics().getTotalTime());
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream( AndroidMock.verify(contentProviderUtils);
VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes()); verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter( }
getContext(), contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId);
assertEquals(0, track.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils); /**
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L); * Test an invalid xml input.
} */
public void testInvalidXml() throws Exception {
testInvalidGpx(INVALID_XML_GPX);
}
/** /**
* Test an invalid xml input. * Test an invalid location.
*/ */
public void testInvalidXml() throws Exception { public void testInvalidLocation() throws Exception {
testInvalidGpx(INVALID_XML_GPX); testInvalidGpx(INVALID_LOCATION_GPX);
} }
/** /**
* Test an invalid location. * Test an invalid time.
*/ */
public void testInvalidLocation() throws Exception { public void testInvalidTime() throws Exception {
testInvalidGpx(INVALID_LOCATION_GPX); testInvalidGpx(INVALID_TIME_GPX);
} }
/** /**
* Test an invalid time. * Test an invalid altitude.
*/ */
public void testInvalidTime() throws Exception { public void testInvalidAltitude() throws Exception {
testInvalidGpx(INVALID_TIME_GPX); testInvalidGpx(INVALID_ALTITUDE_GPX);
} }
/** /**
* Test an invalid altitude. * Test an invalid latitude.
*/ */
public void testInvalidAltitude() throws Exception { public void testInvalidLatitude() throws Exception {
testInvalidGpx(INVALID_ALTITUDE_GPX); testInvalidGpx(INVALID_LATITUDE_GPX);
} }
/** /**
* Test an invalid latitude. * Test an invalid longitude.
*/ */
public void testInvalidLatitude() throws Exception { public void testInvalidLongitude() throws Exception {
testInvalidGpx(INVALID_LATITUDE_GPX); testInvalidGpx(INVALID_LONGITUDE_GPX);
} }
/** private void testInvalidGpx(String xml) throws Exception {
* Test an invalid longitude. expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
*/ .andReturn(TRACK_ID_0_URI);
public void testInvalidLongitude() throws Exception {
testInvalidGpx(INVALID_LONGITUDE_GPX);
}
private void testInvalidGpx(String xml) throws Exception { // For the following, use StubReturn since we don't care whether they are
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject())) // invoked or not.
.andReturn(TRACK_ID_0_URI); expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), AndroidMock.anyInt(), AndroidMock.anyLong()))
.andStubReturn(1);
expect(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).andStubReturn(TRACK_POINT_ID_0);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andStubReturn(TRACK_POINT_ID_0);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
contentProviderUtils.deleteTrack(getContext(), TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
// For the following, use StubReturn since we don't care whether they are InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
// invoked or not. GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
expect(contentProviderUtils.bulkInsertTrackPoint( getContext(), contentProviderUtils);
(Location[]) AndroidMock.anyObject(), AndroidMock.anyInt(), AndroidMock.anyLong())) long trackId = gpxFileTrackImporter.importFile(inputStream);
.andStubReturn(1); assertEquals(-1L, trackId);
expect(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).andStubReturn(TRACK_POINT_ID_0); AndroidMock.verify(contentProviderUtils);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andStubReturn(TRACK_POINT_ID_0); }
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
contentProviderUtils.deleteTrack(getContext(), TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
getContext(), contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream);
assertEquals(-1L, trackId);
AndroidMock.verify(contentProviderUtils);
}
} }
@@ -16,20 +16,20 @@
package com.google.android.apps.mytracks.io.file.importer; package com.google.android.apps.mytracks.io.file.importer;
import static com.google.android.testing.mocking.AndroidMock.eq; import android.location.Location;
import static com.google.android.testing.mocking.AndroidMock.expect;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.AndroidMock;
import android.location.Location; import org.easymock.Capture;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import org.easymock.Capture; import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
/** /**
* Tests for {@link KmlFileTrackImporter}. * Tests for {@link KmlFileTrackImporter}.
@@ -38,104 +38,104 @@ import org.easymock.Capture;
*/ */
public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter { public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
private static String getNameAndDescription(String name, String description) { private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX =
return "<name><![CDATA[" + name + "]]></name><description><![CDATA[" + description "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ "]]></description>"; + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
} + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX =
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>"
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>";
private static String getTrackPoint(int index, String time) { private static String getNameAndDescription(String name, String description) {
String latitude = Double.toString(TRACK_LATITUDE + index); return "<name><![CDATA[" + name + "]]></name><description><![CDATA[" + description
String longitude = Double.toString(TRACK_LONGITUDE + index); + "]]></description>";
String altitude = Double.toString(TRACK_ELEVATION + index); }
StringBuffer buffer = new StringBuffer();
buffer.append("<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " "
+ altitude + "</gx:coord>");
return buffer.toString();
}
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = private static String getTrackPoint(int index, String time) {
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>" String latitude = Double.toString(TRACK_LATITUDE + index);
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>" String longitude = Double.toString(TRACK_LONGITUDE + index);
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) String altitude = Double.toString(TRACK_ELEVATION + index);
+ "</gx:Track></gx:MultiTrack></Placemark></kml>"; StringBuffer buffer = new StringBuffer();
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = buffer.append("<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " "
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>" + altitude + "</gx:coord>");
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>" return buffer.toString();
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>" }
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>";
/** /**
* Tests one track with one segment. * Tests one track with one segment.
*/ */
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
Capture<Track> track = new Capture<Track>(); Capture<Track> track = new Capture<Track>();
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
contentProviderUtils.clearTrack(getContext(), TRACK_ID_0); contentProviderUtils.clearTrack(getContext(), TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint( expect(contentProviderUtils.bulkInsertTrackPoint(
LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1); LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1); expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1);
expect( expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(), contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null); R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0); expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils); AndroidMock.replay(contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter( KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(
getContext(), TRACK_ID_0, contentProviderUtils); getContext(), TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId); assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime()); assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils); AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
/** /**
* Tests one track with two segments. * Tests one track with two segments.
*/ */
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
Capture<Track> track = new Capture<Track>(); Capture<Track> track = new Capture<Track>();
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(getContext(), TRACK_ID_0); contentProviderUtils.clearTrack(getContext(), TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint( expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5); (Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3); expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect( expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(), contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null); R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0); expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils); AndroidMock.replay(contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter( KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(
getContext(), TRACK_ID_0, contentProviderUtils); getContext(), TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId); assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime(); long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
assertEquals( assertEquals(
time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime()); time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils); AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0,
DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
} }
@@ -16,14 +16,14 @@
package com.google.android.apps.mytracks.io.file.importer; package com.google.android.apps.mytracks.io.file.importer;
import com.google.android.testing.mocking.AndroidMock;
import android.location.Location; import android.location.Location;
import java.util.Arrays; import com.google.android.testing.mocking.AndroidMock;
import org.easymock.IArgumentMatcher; import org.easymock.IArgumentMatcher;
import java.util.Arrays;
/** /**
* Locations matcher. Workaround because of capture bug 2617107 in easymock: * Locations matcher. Workaround because of capture bug 2617107 in easymock:
* http://sourceforge.net/tracker/?func=detail&aid=2617107&group_id=82958&atid=567837 * http://sourceforge.net/tracker/?func=detail&aid=2617107&group_id=82958&atid=567837
@@ -32,50 +32,50 @@ import org.easymock.IArgumentMatcher;
*/ */
public class LocationsMatcher implements IArgumentMatcher { public class LocationsMatcher implements IArgumentMatcher {
public static Location[] eqLoc(Location expected) { private final Location[] expectedLocations;
return eqLoc(new Location[] { expected });
}
public static Location[] eqLoc(Location[] expected) { private LocationsMatcher(Location[] expected) {
IArgumentMatcher matcher = new LocationsMatcher(expected); this.expectedLocations = expected;
AndroidMock.reportMatcher(matcher);
return null;
}
private final Location[] expectedLocations;
private LocationsMatcher(Location[] expected) {
this.expectedLocations = expected;
}
@Override
public void appendTo(StringBuffer buf) {
buf.append("eqLoc(").append(Arrays.toString(expectedLocations)).append(")");
}
@Override
public boolean matches(Object obj) {
if (!(obj instanceof Location[])) {
return false;
}
Location[] locations = (Location[]) obj;
if (locations.length < expectedLocations.length) {
return false;
} }
// Only check the first elements (those that will be taken into account) public static Location[] eqLoc(Location expected) {
for (int i = 0; i < expectedLocations.length; i++) { return eqLoc(new Location[]{expected});
if (!matchLocation(locations[i], expectedLocations[i])) {
return false;
}
} }
return true;
}
private boolean matchLocation(Location location1, Location location2) { public static Location[] eqLoc(Location[] expected) {
return (location1.getTime() == location2.getTime()) IArgumentMatcher matcher = new LocationsMatcher(expected);
&& (location1.getLatitude() == location2.getLatitude()) AndroidMock.reportMatcher(matcher);
&& (location1.getLongitude() == location2.getLongitude()) return null;
&& (location1.getAltitude() == location2.getAltitude()); }
}
@Override
public void appendTo(StringBuffer buf) {
buf.append("eqLoc(").append(Arrays.toString(expectedLocations)).append(")");
}
@Override
public boolean matches(Object obj) {
if (!(obj instanceof Location[])) {
return false;
}
Location[] locations = (Location[]) obj;
if (locations.length < expectedLocations.length) {
return false;
}
// Only check the first elements (those that will be taken into account)
for (int i = 0; i < expectedLocations.length; i++) {
if (!matchLocation(locations[i], expectedLocations[i])) {
return false;
}
}
return true;
}
private boolean matchLocation(Location location1, Location location2) {
return (location1.getTime() == location2.getTime())
&& (location1.getLatitude() == location2.getLatitude())
&& (location1.getLongitude() == location2.getLongitude())
&& (location1.getAltitude() == location2.getAltitude());
}
} }
@@ -24,53 +24,53 @@ import junit.framework.TestCase;
*/ */
public class AdaptiveLocationListenerPolicyTest extends TestCase { public class AdaptiveLocationListenerPolicyTest extends TestCase {
private AdaptiveLocationListenerPolicy adocationListenerPolicy; private static final long MIN = 1000;
private static final long MIN = 1000; private static final long MAX = 3000;
private static final long MAX = 3000; private static final int MIN_DISTANCE = 10;
private static final int MIN_DISTANCE = 10; private static final long NEW_IDLE_TIME_BIG = 10000;
private static final long NEW_IDLE_TIME_BIG = 10000; private static final long NEW_IDLE_TIME_NORMAL = 5000;
private static final long NEW_IDLE_TIME_NORMAL = 5000; private static final long NEW_IDLE_TIME_SMALL = 2000;
private static final long NEW_IDLE_TIME_SMALL = 2000; private static final long NEW_IDLE_TIME_LESS_THAN_MIN = 500;
private static final long NEW_IDLE_TIME_LESS_THAN_MIN = 500; private AdaptiveLocationListenerPolicy adocationListenerPolicy;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
adocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE); adocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE);
} }
/** /**
* Tests the * Tests the
* {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four * {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four
* situations. * situations.
* <ul> * <ul>
* <li>The newIdleTime is bigger than max interval.</li> * <li>The newIdleTime is bigger than max interval.</li>
* <li>The newIdleTime is between min and max interval.</li> * <li>The newIdleTime is between min and max interval.</li>
* <li>The newIdleTime is smaller than max interval.</li> * <li>The newIdleTime is smaller than max interval.</li>
* <li>The newIdleTime is smaller than the smallest interval unit.</li> * <li>The newIdleTime is smaller than the smallest interval unit.</li>
* </ul> * </ul>
*/ */
public void testGetDesiredPollingInterval() { public void testGetDesiredPollingInterval() {
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG); adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
assertEquals(MAX, adocationListenerPolicy.getDesiredPollingInterval()); assertEquals(MAX, adocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL); adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the // First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the
// nearest second. // nearest second.
assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000,
adocationListenerPolicy.getDesiredPollingInterval()); adocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL); adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval()); assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN); adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval()); assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval());
} }
/** /**
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance()}. * Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance()}.
*/ */
public void testGetMinDistance() { public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance()); assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance());
} }
} }
@@ -15,14 +15,14 @@
*/ */
package com.google.android.apps.mytracks.services; package com.google.android.apps.mytracks.services;
import com.google.android.maps.mytracks.R;
import com.google.android.testing.mocking.UsesMocks;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.RemoteException; import android.os.RemoteException;
import android.test.ServiceTestCase; import android.test.ServiceTestCase;
import com.google.android.maps.mytracks.R;
import com.google.android.testing.mocking.UsesMocks;
import org.easymock.EasyMock; import org.easymock.EasyMock;
/** /**
@@ -32,74 +32,74 @@ import org.easymock.EasyMock;
*/ */
public class ControlRecordingServiceTest extends ServiceTestCase<ControlRecordingService> { public class ControlRecordingServiceTest extends ServiceTestCase<ControlRecordingService> {
private Context context; private Context context;
private ControlRecordingService controlRecordingService; private ControlRecordingService controlRecordingService;
public ControlRecordingServiceTest() { public ControlRecordingServiceTest() {
super(ControlRecordingService.class); super(ControlRecordingService.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
context = getContext();
}
/**
* Tests the start of {@link ControlRecordingService} and tests the method
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to start a track recording.
*/
@UsesMocks(ITrackRecordingService.class)
public void testStartRecording() {
assertNull(controlRecordingService);
Intent intent = startControlRecordingService(context.getString(R.string.track_action_start));
assertNotNull(controlRecordingService);
ITrackRecordingService iTrackRecordingServiceMock = EasyMock
.createStrictMock(ITrackRecordingService.class);
try {
EasyMock.expect(iTrackRecordingServiceMock.startNewTrack()).andReturn(1L);
EasyMock.replay(iTrackRecordingServiceMock);
controlRecordingService.onHandleIntent(intent, iTrackRecordingServiceMock);
EasyMock.verify(iTrackRecordingServiceMock);
} catch (RemoteException e) {
fail();
} }
}
/** @Override
* Tests the method protected void setUp() throws Exception {
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)} super.setUp();
* to stop a track recording. context = getContext();
*/
@UsesMocks(ITrackRecordingService.class)
public void testStopRecording() {
Intent intent = startControlRecordingService(context.getString(R.string.track_action_end));
ITrackRecordingService iTrackRecordingServiceMock = EasyMock
.createStrictMock(ITrackRecordingService.class);
try {
iTrackRecordingServiceMock.endCurrentTrack();
EasyMock.replay(iTrackRecordingServiceMock);
controlRecordingService.onHandleIntent(intent, iTrackRecordingServiceMock);
EasyMock.verify(iTrackRecordingServiceMock);
} catch (RemoteException e) {
fail();
} }
}
/** /**
* Starts a ControlRecordingService with a specified action. * Tests the start of {@link ControlRecordingService} and tests the method
* * {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* @param action the action string in the start intent * to start a track recording.
*/ */
private Intent startControlRecordingService(String action) { @UsesMocks(ITrackRecordingService.class)
Intent intent = new Intent(context, ControlRecordingService.class); public void testStartRecording() {
intent.setAction(action); assertNull(controlRecordingService);
startService(intent); Intent intent = startControlRecordingService(context.getString(R.string.track_action_start));
controlRecordingService = getService(); assertNotNull(controlRecordingService);
return intent;
} ITrackRecordingService iTrackRecordingServiceMock = EasyMock
.createStrictMock(ITrackRecordingService.class);
try {
EasyMock.expect(iTrackRecordingServiceMock.startNewTrack()).andReturn(1L);
EasyMock.replay(iTrackRecordingServiceMock);
controlRecordingService.onHandleIntent(intent, iTrackRecordingServiceMock);
EasyMock.verify(iTrackRecordingServiceMock);
} catch (RemoteException e) {
fail();
}
}
/**
* Tests the method
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to stop a track recording.
*/
@UsesMocks(ITrackRecordingService.class)
public void testStopRecording() {
Intent intent = startControlRecordingService(context.getString(R.string.track_action_end));
ITrackRecordingService iTrackRecordingServiceMock = EasyMock
.createStrictMock(ITrackRecordingService.class);
try {
iTrackRecordingServiceMock.endCurrentTrack();
EasyMock.replay(iTrackRecordingServiceMock);
controlRecordingService.onHandleIntent(intent, iTrackRecordingServiceMock);
EasyMock.verify(iTrackRecordingServiceMock);
} catch (RemoteException e) {
fail();
}
}
/**
* Starts a ControlRecordingService with a specified action.
*
* @param action the action string in the start intent
*/
private Intent startControlRecordingService(String action) {
Intent intent = new Intent(context, ControlRecordingService.class);
intent.setAction(action);
startService(intent);
controlRecordingService = getService();
return intent;
}
} }
@@ -30,9 +30,9 @@ import org.junit.runner.RunWith;
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class AnnouncementPeriodicTaskFactoryTest { public class AnnouncementPeriodicTaskFactoryTest {
public void testCreate() { public void testCreate() {
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory(); PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
PeriodicTask task = factory.create(InstrumentationRegistry.getInstrumentation().getContext()); PeriodicTask task = factory.create(InstrumentationRegistry.getInstrumentation().getContext());
Assert.assertTrue(task instanceof AnnouncementPeriodicTask); Assert.assertTrue(task instanceof AnnouncementPeriodicTask);
} }
} }
@@ -15,16 +15,6 @@
*/ */
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.testing.mocking.AndroidMock.capture;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import static com.google.android.testing.mocking.AndroidMock.same;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import android.content.Context; import android.content.Context;
import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener; import android.speech.tts.TextToSpeech.OnInitListener;
@@ -33,501 +23,530 @@ import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import java.util.HashMap; import com.google.android.apps.mytracks.stats.TripStatistics;
import java.util.Locale; import com.google.android.apps.mytracks.util.StringUtils;
import java.util.concurrent.atomic.AtomicBoolean; import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import org.easymock.Capture; import org.easymock.Capture;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.android.testing.mocking.AndroidMock.capture;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import static com.google.android.testing.mocking.AndroidMock.same;
/** /**
* Tests for {@link AnnouncementPeriodicTask}. * Tests for {@link AnnouncementPeriodicTask}.
* WARNING: I'm not responsible if your eyes start bleeding while reading this * WARNING: I'm not responsible if your eyes start bleeding while reading this
* code. You have been warned. It's still better than no test, though. * code. You have been warned. It's still better than no test, though.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class AnnouncementPeriodicTaskTest extends AndroidTestCase { public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
// Use something other than our hardcoded value // Use something other than our hardcoded value
private static final Locale DEFAULT_LOCALE = Locale.KOREAN; private static final Locale DEFAULT_LOCALE = Locale.KOREAN;
private static final String ANNOUNCEMENT = "I can haz cheeseburger?"; private static final String ANNOUNCEMENT = "I can haz cheeseburger?";
private Locale oldDefaultLocale; private Locale oldDefaultLocale;
private AnnouncementPeriodicTask task; private AnnouncementPeriodicTask task;
private AnnouncementPeriodicTask mockTask; private AnnouncementPeriodicTask mockTask;
private Capture<OnInitListener> initListenerCapture; private Capture<OnInitListener> initListenerCapture;
private Capture<PhoneStateListener> phoneListenerCapture; private Capture<PhoneStateListener> phoneListenerCapture;
private TextToSpeechDelegate ttsDelegate; private TextToSpeechDelegate ttsDelegate;
private TextToSpeechInterface tts; private TextToSpeechInterface tts;
/**
* Mockable interface that we delegate TTS calls to.
*/
interface TextToSpeechInterface {
int addEarcon(String earcon, String packagename, int resourceId);
int addEarcon(String earcon, String filename);
int addSpeech(String text, String packagename, int resourceId);
int addSpeech(String text, String filename);
boolean areDefaultsEnforced();
String getDefaultEngine();
Locale getLanguage();
int isLanguageAvailable(Locale loc);
boolean isSpeaking();
int playEarcon(String earcon, int queueMode,
HashMap<String, String> params);
int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params);
int setEngineByPackageName(String enginePackageName);
int setLanguage(Locale loc);
int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener);
int setPitch(float pitch);
int setSpeechRate(float speechRate);
void shutdown();
int speak(String text, int queueMode, HashMap<String, String> params);
int stop();
int synthesizeToFile(String text, HashMap<String, String> params,
String filename);
}
/**
* Subclass of {@link TextToSpeech} which delegates calls to the interface
* above.
* The logic here is stupid and the author is ashamed of having to write it
* like this, but basically the issue is that TextToSpeech cannot be mocked
* without running its constructor, its constructor runs async operations
* which call other methods (and then if the methods are part of a mock we'd
* have to set a behavior, but we can't 'cause the object hasn't been fully
* built yet).
* The logic is that calls made during the constructor (when tts is not yet
* set) will go up to the original class, but after tts is set we'll forward
* them all to the mock.
*/
private class TextToSpeechDelegate
extends TextToSpeech implements TextToSpeechInterface {
public TextToSpeechDelegate(Context context, OnInitListener listener) {
super(context, listener);
}
@UsesMocks({
AnnouncementPeriodicTask.class,
StringUtils.class,
})
@Override @Override
public int addEarcon(String earcon, String packagename, int resourceId) { protected void setUp() throws Exception {
if (tts == null) { super.setUp();
return super.addEarcon(earcon, packagename, resourceId);
}
return tts.addEarcon(earcon, packagename, resourceId);
}
@Override oldDefaultLocale = Locale.getDefault();
public int addEarcon(String earcon, String filename) { Locale.setDefault(DEFAULT_LOCALE);
if (tts == null) {
return super.addEarcon(earcon, filename);
}
return tts.addEarcon(earcon, filename);
}
@Override // Eww, the effort required just to mock TextToSpeech is insane
public int addSpeech(String text, String packagename, int resourceId) { final AtomicBoolean listenerCalled = new AtomicBoolean();
if (tts == null) { OnInitListener blockingListener = new OnInitListener() {
return super.addSpeech(text, packagename, resourceId); @Override
} public void onInit(int status) {
return tts.addSpeech(text, packagename, resourceId); synchronized (this) {
} listenerCalled.set(true);
notify();
}
}
};
@Override ttsDelegate = new TextToSpeechDelegate(getContext(), blockingListener);
public int addSpeech(String text, String filename) {
if (tts == null) {
return super.addSpeech(text, filename);
}
return tts.addSpeech(text, filename);
}
@Override // Wait for all async operations done in the constructor to finish.
public Locale getLanguage() { synchronized (blockingListener) {
if (tts == null) { while (!listenerCalled.get()) {
return super.getLanguage(); // Releases the synchronized lock until we're woken up.
} blockingListener.wait();
return tts.getLanguage(); }
}
@Override
public int isLanguageAvailable(Locale loc) {
if (tts == null) {
return super.isLanguageAvailable(loc);
}
return tts.isLanguageAvailable(loc);
}
@Override
public boolean isSpeaking() {
if (tts == null) {
return super.isSpeaking();
}
return tts.isSpeaking();
}
@Override
public int playEarcon(String earcon, int queueMode,
HashMap<String, String> params) {
if (tts == null) {
return super.playEarcon(earcon, queueMode, params);
}
return tts.playEarcon(earcon, queueMode, params);
}
@Override
public int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params) {
if (tts == null) {
return super.playSilence(durationInMs, queueMode, params);
}
return tts.playSilence(durationInMs, queueMode, params);
}
@Override
public int setLanguage(Locale loc) {
if (tts == null) {
return super.setLanguage(loc);
}
return tts.setLanguage(loc);
}
@Override
public int setOnUtteranceCompletedListener(
OnUtteranceCompletedListener listener) {
if (tts == null) {
return super.setOnUtteranceCompletedListener(listener);
}
return tts.setOnUtteranceCompletedListener(listener);
}
@Override
public int setPitch(float pitch) {
if (tts == null) {
return super.setPitch(pitch);
}
return tts.setPitch(pitch);
}
@Override
public int setSpeechRate(float speechRate) {
if (tts == null) {
return super.setSpeechRate(speechRate);
}
return tts.setSpeechRate(speechRate);
}
@Override
public void shutdown() {
if (tts == null) {
super.shutdown();
return;
}
tts.shutdown();
}
@Override
public int speak(
String text, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.speak(text, queueMode, params);
}
return tts.speak(text, queueMode, params);
}
@Override
public int stop() {
if (tts == null) {
return super.stop();
}
return tts.stop();
}
@Override
public int synthesizeToFile(String text, HashMap<String, String> params,
String filename) {
if (tts == null) {
return super.synthesizeToFile(text, params, filename);
}
return tts.synthesizeToFile(text, params, filename);
}
}
@UsesMocks({
AnnouncementPeriodicTask.class,
StringUtils.class,
})
@Override
protected void setUp() throws Exception {
super.setUp();
oldDefaultLocale = Locale.getDefault();
Locale.setDefault(DEFAULT_LOCALE);
// Eww, the effort required just to mock TextToSpeech is insane
final AtomicBoolean listenerCalled = new AtomicBoolean();
OnInitListener blockingListener = new OnInitListener() {
@Override
public void onInit(int status) {
synchronized (this) {
listenerCalled.set(true);
notify();
} }
}
};
ttsDelegate = new TextToSpeechDelegate(getContext(), blockingListener); // Phew, done, now we can start forwarding calls
tts = AndroidMock.createMock(TextToSpeechInterface.class);
// Wait for all async operations done in the constructor to finish. initListenerCapture = new Capture<OnInitListener>();
synchronized (blockingListener) { phoneListenerCapture = new Capture<PhoneStateListener>();
while (!listenerCalled.get()) {
// Releases the synchronized lock until we're woken up. // Create a partial forwarding mock
blockingListener.wait(); mockTask = AndroidMock.createMock(AnnouncementPeriodicTask.class, getContext());
} task = new AnnouncementPeriodicTask(getContext()) {
@Override
protected TextToSpeech newTextToSpeech(Context ctx,
OnInitListener onInitListener) {
return mockTask.newTextToSpeech(ctx, onInitListener);
}
@Override
protected String getAnnouncement(TripStatistics stats) {
return mockTask.getAnnouncement(stats);
}
@Override
protected void listenToPhoneState(
PhoneStateListener listener, int events) {
mockTask.listenToPhoneState(listener, events);
}
};
} }
// Phew, done, now we can start forwarding calls @Override
tts = AndroidMock.createMock(TextToSpeechInterface.class); protected void tearDown() {
Locale.setDefault(oldDefaultLocale);
}
initListenerCapture = new Capture<OnInitListener>(); public void testStart() {
phoneListenerCapture = new Capture<PhoneStateListener>(); doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
// Create a partial forwarding mock AndroidMock.replay(tts);
mockTask = AndroidMock.createMock(AnnouncementPeriodicTask.class, getContext());
task = new AnnouncementPeriodicTask(getContext()) {
@Override
protected TextToSpeech newTextToSpeech(Context ctx,
OnInitListener onInitListener) {
return mockTask.newTextToSpeech(ctx, onInitListener);
}
@Override ttsInitListener.onInit(TextToSpeech.SUCCESS);
protected String getAnnouncement(TripStatistics stats) {
return mockTask.getAnnouncement(stats);
}
@Override AndroidMock.verify(mockTask, tts);
protected void listenToPhoneState( }
PhoneStateListener listener, int events) {
mockTask.listenToPhoneState(listener, events);
}
};
}
@Override public void testStart_notReady() {
protected void tearDown() { doStart();
Locale.setDefault(oldDefaultLocale); OnInitListener ttsInitListener = initListenerCapture.getValue();
} assertNotNull(ttsInitListener);
public void testStart() { AndroidMock.replay(tts);
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
AndroidMock.replay(tts); ttsInitListener.onInit(TextToSpeech.ERROR);
ttsInitListener.onInit(TextToSpeech.SUCCESS); AndroidMock.verify(mockTask, tts);
}
AndroidMock.verify(mockTask, tts); public void testShutdown() {
} // First, start
doStart();
AndroidMock.verify(mockTask);
AndroidMock.reset(mockTask);
public void testStart_notReady() { // Then, shut down
doStart(); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
OnInitListener ttsInitListener = initListenerCapture.getValue(); mockTask.listenToPhoneState(
assertNotNull(ttsInitListener); same(phoneListener), eq(PhoneStateListener.LISTEN_NONE));
tts.shutdown();
AndroidMock.replay(mockTask, tts);
task.shutdown();
AndroidMock.verify(mockTask, tts);
}
AndroidMock.replay(tts); public void testRun() throws Exception {
// Expect service data calls
TripStatistics stats = new TripStatistics();
ttsInitListener.onInit(TextToSpeech.ERROR); // Expect announcement building call
expect(mockTask.getAnnouncement(same(stats))).andStubReturn(ANNOUNCEMENT);
AndroidMock.verify(mockTask, tts); // Put task in "ready" state
} startTask(TextToSpeech.SUCCESS);
public void testShutdown() { expect(tts.isLanguageAvailable(DEFAULT_LOCALE)).andStubReturn(TextToSpeech.LANG_AVAILABLE);
// First, start expect(tts.setLanguage(DEFAULT_LOCALE)).andReturn(TextToSpeech.LANG_AVAILABLE);
doStart(); expect(tts.setSpeechRate(AnnouncementPeriodicTask.TTS_SPEECH_RATE)).andReturn(
AndroidMock.verify(mockTask); TextToSpeech.SUCCESS);
AndroidMock.reset(mockTask); expect(tts.setOnUtteranceCompletedListener((OnUtteranceCompletedListener) EasyMock.anyObject()))
.andReturn(0);
// Then, shut down // Expect actual announcement call
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); expect(
mockTask.listenToPhoneState( tts.speak(eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
same(phoneListener), eq(PhoneStateListener.LISTEN_NONE)); eq(AnnouncementPeriodicTask.SPEECH_PARAMS))).andReturn(0);
tts.shutdown();
AndroidMock.replay(mockTask, tts);
task.shutdown();
AndroidMock.verify(mockTask, tts);
}
public void testRun() throws Exception { // Run the announcement
// Expect service data calls AndroidMock.replay(tts);
TripStatistics stats = new TripStatistics(); task.announce(stats);
AndroidMock.verify(mockTask, tts);
}
// Expect announcement building call public void testRun_notReady() throws Exception {
expect(mockTask.getAnnouncement(same(stats))).andStubReturn(ANNOUNCEMENT); // Put task in "not ready" state
startTask(TextToSpeech.ERROR);
// Put task in "ready" state // Run the announcement
startTask(TextToSpeech.SUCCESS); AndroidMock.replay(tts);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
expect(tts.isLanguageAvailable(DEFAULT_LOCALE)).andStubReturn(TextToSpeech.LANG_AVAILABLE); public void testRun_duringCall() throws Exception {
expect(tts.setLanguage(DEFAULT_LOCALE)).andReturn(TextToSpeech.LANG_AVAILABLE); startTask(TextToSpeech.SUCCESS);
expect(tts.setSpeechRate(AnnouncementPeriodicTask.TTS_SPEECH_RATE)).andReturn(
TextToSpeech.SUCCESS);
expect(tts.setOnUtteranceCompletedListener((OnUtteranceCompletedListener) EasyMock.anyObject()))
.andReturn(0);
// Expect actual announcement call expect(tts.isSpeaking()).andStubReturn(false);
expect(
tts.speak(eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
eq(AnnouncementPeriodicTask.SPEECH_PARAMS))).andReturn(0);
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.announce(stats); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
AndroidMock.verify(mockTask, tts); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
} task.run(null);
AndroidMock.verify(mockTask, tts);
}
public void testRun_notReady() throws Exception { public void testRun_ringWhileSpeaking() throws Exception {
// Put task in "not ready" state startTask(TextToSpeech.SUCCESS);
startTask(TextToSpeech.ERROR);
// Run the announcement expect(tts.isSpeaking()).andStubReturn(true);
AndroidMock.replay(tts); expect(tts.stop()).andReturn(TextToSpeech.SUCCESS);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
public void testRun_duringCall() throws Exception { AndroidMock.replay(tts);
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(false); // Update the state to ringing - this should stop the current announcement.
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement // Run the announcement - this should do nothing.
AndroidMock.replay(tts); task.run(null);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
public void testRun_ringWhileSpeaking() throws Exception { AndroidMock.verify(mockTask, tts);
startTask(TextToSpeech.SUCCESS); }
expect(tts.isSpeaking()).andStubReturn(true); public void testRun_whileRinging() throws Exception {
expect(tts.stop()).andReturn(TextToSpeech.SUCCESS); startTask(TextToSpeech.SUCCESS);
AndroidMock.replay(tts); expect(tts.isSpeaking()).andStubReturn(false);
// Update the state to ringing - this should stop the current announcement. // Run the announcement
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); AndroidMock.replay(tts);
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
// Run the announcement - this should do nothing. public void testRun_noService() throws Exception {
task.run(null); startTask(TextToSpeech.SUCCESS);
AndroidMock.verify(mockTask, tts); // Run the announcement
} AndroidMock.replay(tts);
task.run(null);
AndroidMock.verify(mockTask, tts);
}
public void testRun_whileRinging() throws Exception { public void testRun_noStats() throws Exception {
startTask(TextToSpeech.SUCCESS); // Expect service data calls
expect(tts.isSpeaking()).andStubReturn(false); startTask(TextToSpeech.SUCCESS);
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); task.run(null);
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); AndroidMock.verify(mockTask, tts);
task.run(null); }
AndroidMock.verify(mockTask, tts);
}
public void testRun_noService() throws Exception { /**
startTask(TextToSpeech.SUCCESS); * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time zero.
*/
public void testGetAnnounceTime_time_zero() {
long time = 0; // 0 seconds
assertEquals("0 minutes 0 seconds", task.getAnnounceTime(time));
}
// Run the announcement /**
AndroidMock.replay(tts); * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time one.
task.run(null); */
AndroidMock.verify(mockTask, tts); public void testGetAnnounceTime_time_one() {
} long time = 1 * 1000; // 1 second
assertEquals("0 minutes 1 second", task.getAnnounceTime(time));
}
public void testRun_noStats() throws Exception { /**
// Expect service data calls * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers with the hour unit.
*/
public void testGetAnnounceTime_singular_has_hour() {
long time = (1 * 60 * 60 * 1000) + (1 * 60 * 1000) + (1 * 1000); // 1 hour 1 minute 1 second
assertEquals("1 hour 1 minute 1 second", task.getAnnounceTime(time));
}
startTask(TextToSpeech.SUCCESS); /**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* with the hour unit.
*/
public void testGetAnnounceTime_plural_has_hour() {
long time = (2 * 60 * 60 * 1000) + (2 * 60 * 1000) + (2 * 1000); // 2 hours 2 minutes 2 seconds
assertEquals("2 hours 2 minutes 2 seconds", task.getAnnounceTime(time));
}
// Run the announcement /**
AndroidMock.replay(tts); * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
task.run(null); * numbers without the hour unit.
AndroidMock.verify(mockTask, tts); */
} public void testGetAnnounceTime_singular_no_hour() {
long time = (1 * 60 * 1000) + (1 * 1000); // 1 minute 1 second
assertEquals("1 minute 1 second", task.getAnnounceTime(time));
}
/** /**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time zero. * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
*/ * without the hour unit.
public void testGetAnnounceTime_time_zero() { */
long time = 0; // 0 seconds public void testGetAnnounceTime_plural_no_hour() {
assertEquals("0 minutes 0 seconds", task.getAnnounceTime(time)); long time = (2 * 60 * 1000) + (2 * 1000); // 2 minutes 2 seconds
} assertEquals("2 minutes 2 seconds", task.getAnnounceTime(time));
}
/** private void startTask(int state) {
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time one. AndroidMock.resetToNice(tts);
*/ AndroidMock.replay(tts);
public void testGetAnnounceTime_time_one() { doStart();
long time = 1 * 1000; // 1 second OnInitListener ttsInitListener = initListenerCapture.getValue();
assertEquals("0 minutes 1 second", task.getAnnounceTime(time)); ttsInitListener.onInit(state);
} AndroidMock.resetToDefault(tts);
}
/** private void doStart() {
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular mockTask.listenToPhoneState(capture(phoneListenerCapture),
* numbers with the hour unit. eq(PhoneStateListener.LISTEN_CALL_STATE));
*/ expect(mockTask.newTextToSpeech(
public void testGetAnnounceTime_singular_has_hour() { same(getContext()), capture(initListenerCapture)))
long time = (1 * 60 * 60 * 1000) + (1 * 60 * 1000) + (1 * 1000); // 1 hour 1 minute 1 second .andStubReturn(ttsDelegate);
assertEquals("1 hour 1 minute 1 second", task.getAnnounceTime(time)); AndroidMock.replay(mockTask);
} task.start();
}
/** /**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers * Mockable interface that we delegate TTS calls to.
* with the hour unit. */
*/ interface TextToSpeechInterface {
public void testGetAnnounceTime_plural_has_hour() { int addEarcon(String earcon, String packagename, int resourceId);
long time = (2 * 60 * 60 * 1000) + (2 * 60 * 1000) + (2 * 1000); // 2 hours 2 minutes 2 seconds
assertEquals("2 hours 2 minutes 2 seconds", task.getAnnounceTime(time));
}
/** int addEarcon(String earcon, String filename);
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers without the hour unit.
*/
public void testGetAnnounceTime_singular_no_hour() {
long time = (1 * 60 * 1000) + (1 * 1000); // 1 minute 1 second
assertEquals("1 minute 1 second", task.getAnnounceTime(time));
}
/** int addSpeech(String text, String packagename, int resourceId);
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* without the hour unit.
*/
public void testGetAnnounceTime_plural_no_hour() {
long time = (2 * 60 * 1000) + (2 * 1000); // 2 minutes 2 seconds
assertEquals("2 minutes 2 seconds", task.getAnnounceTime(time));
}
private void startTask(int state) { int addSpeech(String text, String filename);
AndroidMock.resetToNice(tts);
AndroidMock.replay(tts);
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
ttsInitListener.onInit(state);
AndroidMock.resetToDefault(tts);
}
private void doStart() { boolean areDefaultsEnforced();
mockTask.listenToPhoneState(capture(phoneListenerCapture),
eq(PhoneStateListener.LISTEN_CALL_STATE)); String getDefaultEngine();
expect(mockTask.newTextToSpeech(
same(getContext()), capture(initListenerCapture))) Locale getLanguage();
.andStubReturn(ttsDelegate);
AndroidMock.replay(mockTask); int isLanguageAvailable(Locale loc);
task.start();
} boolean isSpeaking();
int playEarcon(String earcon, int queueMode,
HashMap<String, String> params);
int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params);
int setEngineByPackageName(String enginePackageName);
int setLanguage(Locale loc);
int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener);
int setPitch(float pitch);
int setSpeechRate(float speechRate);
void shutdown();
int speak(String text, int queueMode, HashMap<String, String> params);
int stop();
int synthesizeToFile(String text, HashMap<String, String> params,
String filename);
}
/**
* Subclass of {@link TextToSpeech} which delegates calls to the interface
* above.
* The logic here is stupid and the author is ashamed of having to write it
* like this, but basically the issue is that TextToSpeech cannot be mocked
* without running its constructor, its constructor runs async operations
* which call other methods (and then if the methods are part of a mock we'd
* have to set a behavior, but we can't 'cause the object hasn't been fully
* built yet).
* The logic is that calls made during the constructor (when tts is not yet
* set) will go up to the original class, but after tts is set we'll forward
* them all to the mock.
*/
private class TextToSpeechDelegate
extends TextToSpeech implements TextToSpeechInterface {
public TextToSpeechDelegate(Context context, OnInitListener listener) {
super(context, listener);
}
@Override
public int addEarcon(String earcon, String packagename, int resourceId) {
if (tts == null) {
return super.addEarcon(earcon, packagename, resourceId);
}
return tts.addEarcon(earcon, packagename, resourceId);
}
@Override
public int addEarcon(String earcon, String filename) {
if (tts == null) {
return super.addEarcon(earcon, filename);
}
return tts.addEarcon(earcon, filename);
}
@Override
public int addSpeech(String text, String packagename, int resourceId) {
if (tts == null) {
return super.addSpeech(text, packagename, resourceId);
}
return tts.addSpeech(text, packagename, resourceId);
}
@Override
public int addSpeech(String text, String filename) {
if (tts == null) {
return super.addSpeech(text, filename);
}
return tts.addSpeech(text, filename);
}
@Override
public Locale getLanguage() {
if (tts == null) {
return super.getLanguage();
}
return tts.getLanguage();
}
@Override
public int isLanguageAvailable(Locale loc) {
if (tts == null) {
return super.isLanguageAvailable(loc);
}
return tts.isLanguageAvailable(loc);
}
@Override
public boolean isSpeaking() {
if (tts == null) {
return super.isSpeaking();
}
return tts.isSpeaking();
}
@Override
public int playEarcon(String earcon, int queueMode,
HashMap<String, String> params) {
if (tts == null) {
return super.playEarcon(earcon, queueMode, params);
}
return tts.playEarcon(earcon, queueMode, params);
}
@Override
public int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params) {
if (tts == null) {
return super.playSilence(durationInMs, queueMode, params);
}
return tts.playSilence(durationInMs, queueMode, params);
}
@Override
public int setLanguage(Locale loc) {
if (tts == null) {
return super.setLanguage(loc);
}
return tts.setLanguage(loc);
}
@Override
public int setOnUtteranceCompletedListener(
OnUtteranceCompletedListener listener) {
if (tts == null) {
return super.setOnUtteranceCompletedListener(listener);
}
return tts.setOnUtteranceCompletedListener(listener);
}
@Override
public int setPitch(float pitch) {
if (tts == null) {
return super.setPitch(pitch);
}
return tts.setPitch(pitch);
}
@Override
public int setSpeechRate(float speechRate) {
if (tts == null) {
return super.setSpeechRate(speechRate);
}
return tts.setSpeechRate(speechRate);
}
@Override
public void shutdown() {
if (tts == null) {
super.shutdown();
return;
}
tts.shutdown();
}
@Override
public int speak(
String text, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.speak(text, queueMode, params);
}
return tts.speak(text, queueMode, params);
}
@Override
public int stop() {
if (tts == null) {
return super.stop();
}
return tts.stop();
}
@Override
public int synthesizeToFile(String text, HashMap<String, String> params,
String filename) {
if (tts == null) {
return super.synthesizeToFile(text, params, filename);
}
return tts.synthesizeToFile(text, params, filename);
}
}
} }
@@ -25,81 +25,81 @@ import junit.framework.TestCase;
*/ */
public class TripStatisticsTest extends TestCase { public class TripStatisticsTest extends TestCase {
private TripStatistics statistics; private TripStatistics statistics;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
statistics = new TripStatistics(); statistics = new TripStatistics();
} }
public void testSetBounds() { public void testSetBounds() {
// This is not a trivial setter, conversion happens in it // This is not a trivial setter, conversion happens in it
statistics.setBounds(12345, -34567, 56789, -98765); statistics.setBounds(12345, -34567, 56789, -98765);
assertEquals(12345, statistics.getLeft()); assertEquals(12345, statistics.getLeft());
assertEquals(-34567, statistics.getTop()); assertEquals(-34567, statistics.getTop());
assertEquals(56789, statistics.getRight()); assertEquals(56789, statistics.getRight());
assertEquals(-98765, statistics.getBottom()); assertEquals(-98765, statistics.getBottom());
} }
public void testMerge() { public void testMerge() {
TripStatistics statistics2 = new TripStatistics(); TripStatistics statistics2 = new TripStatistics();
statistics.setStartTime(1000L); // Resulting start time statistics.setStartTime(1000L); // Resulting start time
statistics.setStopTime(2500L); statistics.setStopTime(2500L);
statistics2.setStartTime(3000L); statistics2.setStartTime(3000L);
statistics2.setStopTime(4000L); // Resulting stop time statistics2.setStopTime(4000L); // Resulting stop time
statistics.setTotalTime(1500L); statistics.setTotalTime(1500L);
statistics2.setTotalTime(1000L); // Result: 1500+1000 statistics2.setTotalTime(1000L); // Result: 1500+1000
statistics.setMovingTime(700L); statistics.setMovingTime(700L);
statistics2.setMovingTime(600L); // Result: 700+600 statistics2.setMovingTime(600L); // Result: 700+600
statistics.setTotalDistance(750.0); statistics.setTotalDistance(750.0);
statistics2.setTotalDistance(350.0); // Result: 750+350 statistics2.setTotalDistance(350.0); // Result: 750+350
statistics.setTotalElevationGain(50.0); statistics.setTotalElevationGain(50.0);
statistics2.setTotalElevationGain(850.0); // Result: 850+50 statistics2.setTotalElevationGain(850.0); // Result: 850+50
statistics.setMaxSpeed(60.0); // Resulting max speed statistics.setMaxSpeed(60.0); // Resulting max speed
statistics2.setMaxSpeed(30.0); statistics2.setMaxSpeed(30.0);
statistics.setMaxElevation(1250.0); statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0); // Resulting min elevation statistics.setMinElevation(1200.0); // Resulting min elevation
statistics2.setMaxElevation(3575.0); // Resulting max elevation statistics2.setMaxElevation(3575.0); // Resulting max elevation
statistics2.setMinElevation(2800.0); statistics2.setMinElevation(2800.0);
statistics.setMaxGrade(15.0); statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0); // Resulting min grade statistics.setMinGrade(-25.0); // Resulting min grade
statistics2.setMaxGrade(35.0); // Resulting max grade statistics2.setMaxGrade(35.0); // Resulting max grade
statistics2.setMinGrade(0.0); statistics2.setMinGrade(0.0);
// Resulting bounds: -10000, 35000, 30000, -40000 // Resulting bounds: -10000, 35000, 30000, -40000
statistics.setBounds(-10000, 20000, 30000, -40000); statistics.setBounds(-10000, 20000, 30000, -40000);
statistics2.setBounds(-5000, 35000, 0, 20000); statistics2.setBounds(-5000, 35000, 0, 20000);
statistics.merge(statistics2); statistics.merge(statistics2);
assertEquals(1000L, statistics.getStartTime()); assertEquals(1000L, statistics.getStartTime());
assertEquals(4000L, statistics.getStopTime()); assertEquals(4000L, statistics.getStopTime());
assertEquals(2500L, statistics.getTotalTime()); assertEquals(2500L, statistics.getTotalTime());
assertEquals(1300L, statistics.getMovingTime()); assertEquals(1300L, statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance()); assertEquals(1100.0, statistics.getTotalDistance());
assertEquals(900.0, statistics.getTotalElevationGain()); assertEquals(900.0, statistics.getTotalElevationGain());
assertEquals(statistics.getTotalDistance()/ (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed()); assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed());
assertEquals(-10000, statistics.getLeft()); assertEquals(-10000, statistics.getLeft());
assertEquals(30000, statistics.getRight()); assertEquals(30000, statistics.getRight());
assertEquals(35000, statistics.getTop()); assertEquals(35000, statistics.getTop());
assertEquals(-40000, statistics.getBottom()); assertEquals(-40000, statistics.getBottom());
assertEquals(1200.0, statistics.getMinElevation()); assertEquals(1200.0, statistics.getMinElevation());
assertEquals(3575.0, statistics.getMaxElevation()); assertEquals(3575.0, statistics.getMaxElevation());
assertEquals(-25.0, statistics.getMinGrade()); assertEquals(-25.0, statistics.getMinGrade());
assertEquals(35.0, statistics.getMaxGrade()); assertEquals(35.0, statistics.getMaxGrade());
} }
public void testGetAverageSpeed() { public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0); statistics.setTotalDistance(1000.0);
statistics.setTotalTime(50000); // in milliseconds statistics.setTotalTime(50000); // in milliseconds
assertEquals(20.0, statistics.getAverageSpeed()); assertEquals(20.0, statistics.getAverageSpeed());
} }
public void testGetAverageMovingSpeed() { public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0); statistics.setTotalDistance(1000.0);
statistics.setMovingTime(20000); // in milliseconds statistics.setMovingTime(20000); // in milliseconds
assertEquals(50.0, statistics.getAverageMovingSpeed()); assertEquals(50.0, statistics.getAverageMovingSpeed());
} }
} }
@@ -17,10 +17,10 @@ package com.google.android.apps.mytracks.util;
import android.os.Environment; import android.os.Environment;
import java.io.File;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.io.File;
/** /**
* Tests for {@link FileUtils}. * Tests for {@link FileUtils}.
* *
@@ -28,94 +28,94 @@ import junit.framework.TestCase;
*/ */
public class FileUtilsTest extends TestCase { public class FileUtilsTest extends TestCase {
/** /**
* Tests {@link FileUtils#getPath(String...)}. * Tests {@link FileUtils#getPath(String...)}.
*/ */
public void testBuildExternalDirectoryPath() { public void testBuildExternalDirectoryPath() {
String expectedName = Environment.getExternalStorageDirectory() + File.separator String expectedName = Environment.getExternalStorageDirectory() + File.separator
+ FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator + FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator
+ "c"; + "c";
String dirName = FileUtils.getPath("a", "b", "c"); String dirName = FileUtils.getPath("a", "b", "c");
assertEquals(expectedName, dirName); assertEquals(expectedName, dirName);
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the
* file is new.
*/
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
assertEquals("Filename.ext", filename);
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the
* file exists already.
*/
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop");
assertEquals("default(1).prop", filename);
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special characters.
* Verifies that they are sanitized.
*/
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean)_";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in
* Chinese and Russian). Verifies that they are allowed.
*/
public void testSanitizeFileName_i18n() {
String name = "您好-привет";
String expected = "您好-привет";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32
* characters. Verifies that they are allowed.
*/
public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] ";
String expected = "$%'-_@~`!(){}^#&+,;=[] ";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped
* characters in a row. Verifies that they are collapsed into one underscore.
*/
public void testSanitizeFileName_collapse() {
String name = "hello//there";
String expected = "hello_there";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#truncateFileName(File, String, String)}. Verifies
* the a long file name is truncated.
*/
public void testTruncateFileName() {
File directory = new File("/dir1/dir2/");
String suffix = ".gpx";
char[] name = new char[FileUtils.MAX_FAT32_PATH_LENGTH];
for (int i = 0; i < name.length; i++) {
name[i] = 'a';
} }
String nameString = new String(name);
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
for (int i = 0; i < truncated.length(); i++) { /**
assertEquals('a', truncated.charAt(i)); * Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the
* file is new.
*/
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
assertEquals("Filename.ext", filename);
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the
* file exists already.
*/
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop");
assertEquals("default(1).prop", filename);
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special characters.
* Verifies that they are sanitized.
*/
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean)_";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in
* Chinese and Russian). Verifies that they are allowed.
*/
public void testSanitizeFileName_i18n() {
String name = "您好-привет";
String expected = "您好-привет";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32
* characters. Verifies that they are allowed.
*/
public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] ";
String expected = "$%'-_@~`!(){}^#&+,;=[] ";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped
* characters in a row. Verifies that they are collapsed into one underscore.
*/
public void testSanitizeFileName_collapse() {
String name = "hello//there";
String expected = "hello_there";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#truncateFileName(File, String, String)}. Verifies
* the a long file name is truncated.
*/
public void testTruncateFileName() {
File directory = new File("/dir1/dir2/");
String suffix = ".gpx";
char[] name = new char[FileUtils.MAX_FAT32_PATH_LENGTH];
for (int i = 0; i < name.length; i++) {
name[i] = 'a';
}
String nameString = new String(name);
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i));
}
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH,
new File(directory, truncated + suffix).getPath().length());
} }
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH,
new File(directory, truncated + suffix).getPath().length());
}
} }
@@ -33,156 +33,156 @@ import java.util.TimeZone;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class StringUtilsTest { public class StringUtilsTest {
/** /**
* Tests {@link StringUtils#formatDateTimeIso8601(long)}. * Tests {@link StringUtils#formatDateTimeIso8601(long)}.
*/ */
public void testFormatDateTimeIso8601() { public void testFormatDateTimeIso8601() {
Assert.assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345)); Assert.assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345));
} }
/** /**
* Tests {@link StringUtils#formatElapsedTime(long)}. * Tests {@link StringUtils#formatElapsedTime(long)}.
*/ */
public void testformatElapsedTime() { public void testformatElapsedTime() {
// 1 second // 1 second
Assert.assertEquals("00:01", StringUtils.formatElapsedTime(1000)); Assert.assertEquals("00:01", StringUtils.formatElapsedTime(1000));
// 10 seconds // 10 seconds
Assert.assertEquals("00:10", StringUtils.formatElapsedTime(10000)); Assert.assertEquals("00:10", StringUtils.formatElapsedTime(10000));
// 1 minute // 1 minute
Assert.assertEquals("01:00", StringUtils.formatElapsedTime(60000)); Assert.assertEquals("01:00", StringUtils.formatElapsedTime(60000));
// 10 minutes // 10 minutes
Assert.assertEquals("10:00", StringUtils.formatElapsedTime(600000)); Assert.assertEquals("10:00", StringUtils.formatElapsedTime(600000));
// 1 hour // 1 hour
Assert.assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000)); Assert.assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000));
// 10 hours // 10 hours
Assert.assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000)); Assert.assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000));
// 100 hours // 100 hours
Assert.assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000)); Assert.assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000));
} }
/** /**
* Tests {@link StringUtils#formatElapsedTimeWithHour(long)}. * Tests {@link StringUtils#formatElapsedTimeWithHour(long)}.
*/ */
public void testformatElapsedTimeWithHour() { public void testformatElapsedTimeWithHour() {
// 1 second // 1 second
Assert.assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000)); Assert.assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000));
// 10 seconds // 10 seconds
Assert.assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000)); Assert.assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000));
// 1 minute // 1 minute
Assert.assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000)); Assert.assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000));
// 10 minutes // 10 minutes
Assert.assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000)); Assert.assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000));
// 1 hour // 1 hour
Assert.assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000)); Assert.assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000));
// 10 hours // 10 hours
Assert.assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000)); Assert.assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000));
// 100 hours // 100 hours
Assert.assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000)); Assert.assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000));
} }
/** /**
* Tests {@link StringUtils#formatDistance(android.content.Context, double, * Tests {@link StringUtils#formatDistance(android.content.Context, double,
* boolean)}. * boolean)}.
*/ */
public void testFormatDistance() { public void testFormatDistance() {
// A large number in metric // A large number in metric
Assert.assertEquals("5.00 km", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 5000, true)); Assert.assertEquals("5.00 km", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 5000, true));
// A large number in imperial // A large number in imperial
Assert.assertEquals("3.11 mi", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 5000, false)); Assert.assertEquals("3.11 mi", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 5000, false));
// A small number in metric // A small number in metric
Assert.assertEquals("100.00 m", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 100, true)); Assert.assertEquals("100.00 m", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 100, true));
// A small number in imperial // A small number in imperial
Assert.assertEquals("328.08 ft", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 100, false)); Assert.assertEquals("328.08 ft", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 100, false));
} }
/** /**
* Tests {@link StringUtils#formatCData(String)}. * Tests {@link StringUtils#formatCData(String)}.
*/ */
public void testFormatCData() { public void testFormatCData() {
Assert.assertEquals("<![CDATA[hello]]>", StringUtils.formatCData("hello")); Assert.assertEquals("<![CDATA[hello]]>", StringUtils.formatCData("hello"));
Assert.assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there")); Assert.assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
} }
/** /**
* Tests {@link StringUtils#getTime(String)}. * Tests {@link StringUtils#getTime(String)}.
*/ */
public void testGetTime() { public void testGetTime() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0); assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0); assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
} }
/** /**
* Tests {@link StringUtils#getTime(String)} with fractional seconds. * Tests {@link StringUtils#getTime(String)} with fractional seconds.
*/ */
public void testGetTime_fractional() { public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300); assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300);
assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350); assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350);
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3525Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.3525Z", 2010, 5, 4, 3, 2, 1, 352);
} }
/** /**
* Tests {@link StringUtils#getTime(String)} with time zone. * Tests {@link StringUtils#getTime(String)} with time zone.
*/ */
public void testGetTime_timezone() { public void testGetTime_timezone() {
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0); assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0); assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01-00:00", 2010, 5, 4, 3, 2, 1, 0); assertGetTime("2010-05-04T03:02:01-00:00", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+01:00", 2010, 5, 4, 2, 2, 1, 0); assertGetTime("2010-05-04T03:02:01+01:00", 2010, 5, 4, 2, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+10:30", 2010, 5, 3, 16, 32, 1, 0); assertGetTime("2010-05-04T03:02:01+10:30", 2010, 5, 3, 16, 32, 1, 0);
assertGetTime("2010-05-04T03:02:01-09:30", 2010, 5, 4, 12, 32, 1, 0); assertGetTime("2010-05-04T03:02:01-09:30", 2010, 5, 4, 12, 32, 1, 0);
assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0); assertGetTime("2010-05-04T03:02:01-05:00", 2010, 5, 4, 8, 2, 1, 0);
} }
/** /**
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time * Tests {@link StringUtils#getTime(String)} with fractional seconds and time
* zone. * zone.
*/ */
public void testGetTime_fractionalAndTimezone() { public void testGetTime_fractionalAndTimezone() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470); assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470);
assertGetTime("2010-05-04T03:02:01.5791+03:00", 2010, 5, 4, 0, 2, 1, 579); assertGetTime("2010-05-04T03:02:01.5791+03:00", 2010, 5, 4, 0, 2, 1, 579);
assertGetTime("2010-05-04T03:02:01.8-05:30", 2010, 5, 4, 8, 32, 1, 800); assertGetTime("2010-05-04T03:02:01.8-05:30", 2010, 5, 4, 8, 32, 1, 800);
} }
/** /**
* Asserts the {@link StringUtils#getTime(String)} returns the expected * Asserts the {@link StringUtils#getTime(String)} returns the expected
* values. * values.
* *
* @param xmlDateTime the xml date time string * @param xmlDateTime the xml date time string
* @param year the expected year * @param year the expected year
* @param month the expected month * @param month the expected month
* @param day the expected day * @param day the expected day
* @param hour the expected hour * @param hour the expected hour
* @param minute the expected minute * @param minute the expected minute
* @param second the expected second * @param second the expected second
* @param millisecond the expected milliseconds * @param millisecond the expected milliseconds
*/ */
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute,
int second, int millisecond) { int second, int millisecond) {
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.set(year, month - 1, day, hour, minute, second); calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, millisecond); calendar.set(GregorianCalendar.MILLISECOND, millisecond);
Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.getTime(xmlDateTime)); Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.getTime(xmlDateTime));
} }
/** /**
* Tests {@link StringUtils#getTimeParts(long)} with a positive number. * Tests {@link StringUtils#getTimeParts(long)} with a positive number.
*/ */
public void testGetTimeParts_postive() { public void testGetTimeParts_postive() {
int[] parts = StringUtils.getTimeParts(61000); int[] parts = StringUtils.getTimeParts(61000);
Assert.assertEquals(1, parts[0]); Assert.assertEquals(1, parts[0]);
Assert.assertEquals(1, parts[1]); Assert.assertEquals(1, parts[1]);
Assert.assertEquals(0, parts[2]); Assert.assertEquals(0, parts[2]);
} }
/** /**
* Tests {@link StringUtils#getTimeParts(long)} with a negative number. * Tests {@link StringUtils#getTimeParts(long)} with a negative number.
*/ */
public void testGetTimeParts_negative() { public void testGetTimeParts_negative() {
int[] parts = StringUtils.getTimeParts(-61000); int[] parts = StringUtils.getTimeParts(-61000);
Assert.assertEquals(-1, parts[0]); Assert.assertEquals(-1, parts[0]);
Assert.assertEquals(-1, parts[1]); Assert.assertEquals(-1, parts[1]);
Assert.assertEquals(0, parts[2]); Assert.assertEquals(0, parts[2]);
} }
} }
@@ -38,40 +38,40 @@ import java.util.Locale;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class TrackNameUtilsTest { public class TrackNameUtilsTest {
private static final long TRACK_ID = 1L; private static final long TRACK_ID = 1L;
private static final long START_TIME = 1288213406000L; private static final long START_TIME = 1288213406000L;
private static final Context CONTEXT = InstrumentationRegistry.getInstrumentation().getContext(); private static final Context CONTEXT = InstrumentationRegistry.getInstrumentation().getContext();
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is
* settings_recording_track_name_date_local_value. * settings_recording_track_name_date_local_value.
*/ */
public void testTrackName_date_local() { public void testTrackName_date_local() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value)); PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
Assert.assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), Assert.assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME),
TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null)); TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null));
} }
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is
* settings_recording_track_name_date_iso_8601_value. * settings_recording_track_name_date_iso_8601_value.
*/ */
public void testTrackName_date_iso_8601() { public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value)); PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat( SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
TrackNameUtils.ISO_8601_FORMAT, Locale.US); TrackNameUtils.ISO_8601_FORMAT, Locale.US);
Assert.assertEquals(simpleDateFormat.format(new Date(START_TIME)), Assert.assertEquals(simpleDateFormat.format(new Date(START_TIME)),
TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null)); TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null));
} }
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is
* settings_recording_track_name_number_value. * settings_recording_track_name_number_value.
*/ */
public void testTrackName_number() { public void testTrackName_number() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value)); PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
Assert.assertEquals( Assert.assertEquals(
"Track " + TRACK_ID, TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null)); "Track " + TRACK_ID, TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null));
} }
} }
@@ -27,62 +27,61 @@ import androidx.fragment.app.FragmentActivity;
import com.google.android.apps.mytracks.util.TrackIconUtils; import com.google.android.apps.mytracks.util.TrackIconUtils;
/** /**
*
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public abstract class AbstractActivity extends FragmentActivity { public abstract class AbstractActivity extends FragmentActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Set volume control stream for text to speech // Set volume control stream for text to speech
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setContentView(getLayoutResId()); setContentView(getLayoutResId());
// Configure action bar must be after setContentView // Configure action bar must be after setContentView
if (configureActionBarHomeAsUp()) { if (configureActionBarHomeAsUp()) {
ActionBar actionBar = this.getActionBar(); ActionBar actionBar = this.getActionBar();
if (actionBar != null) { if (actionBar != null) {
actionBar.setHomeButtonEnabled(true); actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
} }
}
} }
}
/** /**
* Gets the layout resource id. * Gets the layout resource id.
*/ */
protected abstract int getLayoutResId(); protected abstract int getLayoutResId();
/** /**
* Returns true to configure the action bar home button as the up button (go to previous activity). * Returns true to configure the action bar home button as the up button (go to previous activity).
*/ */
protected boolean configureActionBarHomeAsUp() { protected boolean configureActionBarHomeAsUp() {
return true; return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
TrackIconUtils.setMenuIconColor(menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handles actionBar's home menu item (arrow top left corner) and triggers, up navigation to previous activity.
if (item.getItemId() == android.R.id.home) {
onHomeSelected();
return true;
} }
return super.onOptionsItemSelected(item);
}
/** @Override
* Callback when the home menu item is selected. E.g., setup the back stack when home is selected. public boolean onCreateOptionsMenu(Menu menu) {
*/ TrackIconUtils.setMenuIconColor(menu);
protected void onHomeSelected() { return super.onCreateOptionsMenu(menu);
finish(); }
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handles actionBar's home menu item (arrow top left corner) and triggers, up navigation to previous activity.
if (item.getItemId() == android.R.id.home) {
onHomeSelected();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Callback when the home menu item is selected. E.g., setup the back stack when home is selected.
*/
protected void onHomeSelected() {
finish();
}
} }
@@ -51,74 +51,65 @@ import java.util.ArrayList;
*/ */
public abstract class AbstractTrackActivity extends AbstractActivity implements ConfirmDeleteCaller { public abstract class AbstractTrackActivity extends AbstractActivity implements ConfirmDeleteCaller {
private static final int DELETE_REQUEST_CODE = 3; protected static final int GPS_REQUEST_CODE = 6;
protected static final int GPS_REQUEST_CODE = 6; private static final int DELETE_REQUEST_CODE = 3;
@Override public static void configureListViewContextualMenu(final ListView listView, final ContextualActionModeCallback contextualActionModeCallback) {
protected void onActivityResult(int requestCode, int resultCode, Intent data) { listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
if (requestCode == DELETE_REQUEST_CODE) { listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
onDeleted();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
public static void configureListViewContextualMenu(final ListView listView, final ContextualActionModeCallback contextualActionModeCallback) { @Override
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); public boolean onCreateActionMode(ActionMode mode, Menu menu) {
listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() { mode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
return true;
}
@Override @Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.list_context_menu, menu); contextualActionModeCallback.onPrepare(menu, getCheckedPositions(listView), listView.getCheckedItemIds(), true);
return true; return true;
} }
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { public void onDestroyActionMode(ActionMode mode) {
contextualActionModeCallback.onPrepare(menu, getCheckedPositions(listView), listView.getCheckedItemIds(), true); // Do nothing
return true; }
}
@Override @Override
public void onDestroyActionMode(ActionMode mode) { public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
// Do nothing mode.invalidate();
} }
@Override @Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
mode.invalidate(); if (contextualActionModeCallback.onClick(item.getItemId(), getCheckedPositions(listView), listView.getCheckedItemIds())) {
} mode.finish();
}
return true;
}
@Override /**
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { * Gets the checked positions in a list view.
if (contextualActionModeCallback.onClick(item.getItemId(), getCheckedPositions(listView), listView.getCheckedItemIds())) { *
mode.finish(); * @param list the list view
} */
return true; private int[] getCheckedPositions(ListView list) {
} SparseBooleanArray positions = list.getCheckedItemPositions();
ArrayList<Integer> arrayList = new ArrayList<>();
/** for (int i = 0; i < positions.size(); i++) {
* Gets the checked positions in a list view. int key = positions.keyAt(i);
* if (positions.valueAt(i)) {
* @param list the list view arrayList.add(key);
*/ }
private int[] getCheckedPositions(ListView list) { }
SparseBooleanArray positions = list.getCheckedItemPositions(); int[] result = new int[arrayList.size()];
ArrayList<Integer> arrayList = new ArrayList<>(); for (int i = 0; i < arrayList.size(); i++) {
for (int i = 0; i < positions.size(); i++) { result[i] = arrayList.get(i);
int key = positions.keyAt(i); }
if (positions.valueAt(i)) { return result;
arrayList.add(key); }
} });
} }
int[] result = new int[arrayList.size()];
for (int i = 0; i < arrayList.size(); i++) {
result[i] = arrayList.get(i);
}
return result;
}
});
}
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) { public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE); SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
@@ -166,46 +157,55 @@ public abstract class AbstractTrackActivity extends AbstractActivity implements
}); });
} }
/** @Override
* Delete tracks. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
* if (requestCode == DELETE_REQUEST_CODE) {
* @param trackIds the track ids onDeleted();
*/ } else {
protected void deleteTracks(long[] trackIds) { super.onActivityResult(requestCode, resultCode, data);
ConfirmDeleteDialogFragment.newInstance(trackIds)
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
}
@Override
public void onConfirmDeleteDone(long[] trackIds) {
boolean stopRecording = false;
if (trackIds.length == 1 && trackIds[0] == -1L) {
stopRecording = true;
} else {
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
for (long trackId : trackIds) {
if (trackId == recordingTrackId) {
stopRecording = true;
break;
} }
}
} }
if (stopRecording) {
TrackRecordingServiceConnectionUtils.stopRecording(this, getTrackRecordingServiceConnection(), false); /**
* Delete tracks.
*
* @param trackIds the track ids
*/
protected void deleteTracks(long[] trackIds) {
ConfirmDeleteDialogFragment.newInstance(trackIds)
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
} }
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
startActivityForResult(intent, DELETE_REQUEST_CODE);
}
/** @Override
* Gets the track recording service connection. For stopping the current public void onConfirmDeleteDone(long[] trackIds) {
* recording if need to delete the current recording track. boolean stopRecording = false;
*/ if (trackIds.length == 1 && trackIds[0] == -1L) {
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection(); stopRecording = true;
} else {
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
for (long trackId : trackIds) {
if (trackId == recordingTrackId) {
stopRecording = true;
break;
}
}
}
if (stopRecording) {
TrackRecordingServiceConnectionUtils.stopRecording(this, getTrackRecordingServiceConnection(), false);
}
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
startActivityForResult(intent, DELETE_REQUEST_CODE);
}
/** /**
* Called after {@link DeleteActivity} returns its result. * Gets the track recording service connection. For stopping the current
*/ * recording if need to delete the current recording track.
abstract protected void onDeleted(); */
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
/**
* Called after {@link DeleteActivity} returns its result.
*/
abstract protected void onDeleted();
} }
@@ -16,14 +16,14 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import android.os.Bundle;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StatsUtils; import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import java.util.List; import java.util.List;
/** /**
@@ -33,31 +33,31 @@ import java.util.List;
*/ */
public class AggregatedStatsActivity extends AbstractActivity { public class AggregatedStatsActivity extends AbstractActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
StatsUtils.setTripStatisticsValues(this, this, null, getTripStatistics(), null); StatsUtils.setTripStatisticsValues(this, this, null, getTripStatistics(), null);
StatsUtils.setLocationValues(this, this, null, null, false); StatsUtils.setLocationValues(this, this, null, null, false);
} }
@Override @Override
protected int getLayoutResId() { protected int getLayoutResId() {
return R.layout.stats; return R.layout.stats;
} }
/** /**
* Gets the aggregated trip statistics for all the recorded tracks or null if * Gets the aggregated trip statistics for all the recorded tracks or null if
* there is no track. * there is no track.
*/ */
private TripStatistics getTripStatistics() { private TripStatistics getTripStatistics() {
List<Track> tracks = ContentProviderUtils.Factory.get(this).getAllTracks(); List<Track> tracks = ContentProviderUtils.Factory.get(this).getAllTracks();
TripStatistics tripStatistics = null; TripStatistics tripStatistics = null;
if (!tracks.isEmpty()) { if (!tracks.isEmpty()) {
tripStatistics = new TripStatistics(tracks.iterator().next().getTripStatistics()); tripStatistics = new TripStatistics(tracks.iterator().next().getTripStatistics());
for (int i = 1; i < tracks.size(); i++) { for (int i = 1; i < tracks.size(); i++) {
tripStatistics.merge(tracks.get(i).getTripStatistics()); tripStatistics.merge(tracks.get(i).getTripStatistics());
} }
}
return tripStatistics;
} }
return tripStatistics;
}
} }
@@ -15,24 +15,24 @@
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
/** /**
* This class react to the BOOT_COMPLETED broadcast. * This class react to the BOOT_COMPLETED broadcast.
* * <p>
* One example of a broadcast message that this class is interested in, * One example of a broadcast message that this class is interested in,
* is notification about the phone boot. We may want to resume a previously * is notification about the phone boot. We may want to resume a previously
* started tracking session if the phone crashed (hopefully not), or the user * started tracking session if the phone crashed (hopefully not), or the user
* decided to swap the battery or some external event occurred which forced * decided to swap the battery or some external event occurred which forced
* a phone reboot. * a phone reboot.
* * <p>
* This class simply delegates to {@link TrackRecordingService} to make a * This class simply delegates to {@link TrackRecordingService} to make a
* decision whether to continue with the previous track (if any), or just * decision whether to continue with the previous track (if any), or just
* abandon it. * abandon it.
@@ -41,17 +41,17 @@ import android.util.Log;
*/ */
public class BootReceiver extends BroadcastReceiver { public class BootReceiver extends BroadcastReceiver {
private static final String TAG = BootReceiver.class.getSimpleName(); private static final String TAG = BootReceiver.class.getSimpleName();
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction()); Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction());
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) { if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent startIntent = new Intent(context, TrackRecordingService.class) Intent startIntent = new Intent(context, TrackRecordingService.class)
.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); .putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent); context.startService(startIntent);
} else { } else {
Log.w(TAG, "BootReceiver: unsupported action"); Log.w(TAG, "BootReceiver: unsupported action");
}
} }
}
} }
@@ -22,6 +22,7 @@ import android.graphics.Paint;
import android.graphics.Paint.Align; import android.graphics.Paint.Align;
import android.graphics.Paint.Style; import android.graphics.Paint.Style;
import android.graphics.Path; import android.graphics.Path;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.apps.mytracks.stats.ExtremityMonitor; import com.google.android.apps.mytracks.stats.ExtremityMonitor;
@@ -35,229 +36,229 @@ import java.text.NumberFormat;
*/ */
public class ChartValueSeries { public class ChartValueSeries {
private static final float STROKE_WIDTH = 2f; private static final float STROKE_WIDTH = 2f;
private final int absoluteMin; private final int absoluteMin;
private final int absoluteMax; private final int absoluteMax;
private final int[] intervalValues; private final int[] intervalValues;
private final int metricTitleId; private final int metricTitleId;
private final int imperialTitleId; private final int imperialTitleId;
private final Paint fillPaint; private final Paint fillPaint;
private final Paint strokePaint; private final Paint strokePaint;
private final Paint titlePaint; private final Paint titlePaint;
private final Paint markerPaint; private final Paint markerPaint;
private final ExtremityMonitor extremityMonitor; private final ExtremityMonitor extremityMonitor;
private final NumberFormat numberFormat; private final NumberFormat numberFormat;
private final Path path; private final Path path;
private int interval = 1; private int interval = 1;
private int minMarkerValue = 0; private int minMarkerValue = 0;
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS; private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
private boolean enabled = true; private boolean enabled = true;
/** /**
* Constructor. * Constructor.
* *
* @param context the context * @param context the context
* @param absoluteMin the absolute min value * @param absoluteMin the absolute min value
* @param absoluteMax the absolute max value * @param absoluteMax the absolute max value
* @param intervalValues the list of interval values * @param intervalValues the list of interval values
* @param metricTitleId the metric title id * @param metricTitleId the metric title id
* @param imperialTitleId the imperial title id * @param imperialTitleId the imperial title id
* @param fillColor the fill color * @param fillColor the fill color
* @param strokeColor the stroke color * @param strokeColor the stroke color
*/ */
public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues,
int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) { int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
this.absoluteMin = absoluteMin; this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax; this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues; this.intervalValues = intervalValues;
this.metricTitleId = metricTitleId; this.metricTitleId = metricTitleId;
this.imperialTitleId = imperialTitleId; this.imperialTitleId = imperialTitleId;
fillPaint = new Paint(); fillPaint = new Paint();
fillPaint.setStyle(Style.FILL); fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor)); fillPaint.setColor(context.getResources().getColor(fillColor));
fillPaint.setAntiAlias(true); fillPaint.setAntiAlias(true);
strokePaint = new Paint(); strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE); strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor)); strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setAntiAlias(true); strokePaint.setAntiAlias(true);
float scale = context.getResources().getDisplayMetrics().density; float scale = context.getResources().getDisplayMetrics().density;
// Make copies of the stroke paint with the default thickness // Make copies of the stroke paint with the default thickness
titlePaint = new Paint(strokePaint); titlePaint = new Paint(strokePaint);
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale); titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
titlePaint.setTextAlign(Align.CENTER); titlePaint.setTextAlign(Align.CENTER);
markerPaint = new Paint(strokePaint); markerPaint = new Paint(strokePaint);
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale); markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
markerPaint.setTextAlign(Align.RIGHT); markerPaint.setTextAlign(Align.RIGHT);
// Set stroke paint thickness // Set stroke paint thickness
strokePaint.setStrokeWidth(STROKE_WIDTH); strokePaint.setStrokeWidth(STROKE_WIDTH);
extremityMonitor = new ExtremityMonitor(); extremityMonitor = new ExtremityMonitor();
numberFormat = NumberFormat.getIntegerInstance(); numberFormat = NumberFormat.getIntegerInstance();
path = new Path(); path = new Path();
}
/**
* Sets the series enabled value.
*
* @param enabled true to enable
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Returns true if the series is enabled.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Returns true if the series has data.
*/
public boolean hasData() {
return extremityMonitor.hasData();
}
/**
* Updates the series with a new value.
*
* @param value the new value
*/
public void update(double value) {
extremityMonitor.update(value);
}
/**
* Gets the path.
*/
public Path getPath() {
return path;
}
/**
* Draws the path on canvas.
*
* @param canvas the canvas
*/
public void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
/**
* Updates the y axis dimension.
*/
public void updateDimension() {
double min = hasData() ? extremityMonitor.getMin() : 0.0;
double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin);
max = Math.min(max, absoluteMax);
interval = getInterval(min, max);
minMarkerValue = getMinMarkerValue(min, interval);
maxMarkerValue = minMarkerValue + interval * ChartView.Y_AXIS_INTERVALS;
}
/**
* Gets the interval value.
*
* @param min the min value
* @param max the max value
*/
private int getInterval(double min, double max) {
for (int intervalValue : intervalValues) {
int minValue = getMinMarkerValue(min, intervalValue);
double targetInterval = (max - minValue) / ChartView.Y_AXIS_INTERVALS;
if (intervalValue >= targetInterval) {
return intervalValue;
}
} }
// Return the largest interval
return intervalValues[intervalValues.length - 1];
}
/** /**
* Gets the min marker value. * Returns true if the series is enabled.
* */
* @param min the min series value public boolean isEnabled() {
* @param intervalValue the interval value return enabled;
*/
private int getMinMarkerValue(double min, int intervalValue) {
// Round down to the nearest intervalValue
int value = ((int) (min / intervalValue)) * intervalValue;
// value > min if min is negative
if (value > min) {
return value - intervalValue;
} }
return value;
}
/** /**
* Gets the interval value. * Sets the series enabled value.
*/ *
public int getInterval() { * @param enabled true to enable
return interval; */
} public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/** /**
* Gets the min marker value. * Returns true if the series has data.
*/ */
public int getMinMarkerValue() { public boolean hasData() {
return minMarkerValue; return extremityMonitor.hasData();
} }
/** /**
* Gets the max marker value. * Updates the series with a new value.
*/ *
@VisibleForTesting * @param value the new value
int getMaxMarkerValue() { */
return maxMarkerValue; public void update(double value) {
} extremityMonitor.update(value);
}
/** /**
* Gets the title id. * Gets the path.
*/ */
public int getTitleId(boolean metricUnits) { public Path getPath() {
return metricUnits ? metricTitleId : imperialTitleId; return path;
} }
/** /**
* Gets the title paint. * Draws the path on canvas.
*/ *
public Paint getTitlePaint() { * @param canvas the canvas
return titlePaint; */
} public void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
/** /**
* Gets the marker paint. * Updates the y axis dimension.
*/ */
public Paint getMarkerPaint() { public void updateDimension() {
return markerPaint; double min = hasData() ? extremityMonitor.getMin() : 0.0;
} double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin);
max = Math.min(max, absoluteMax);
interval = getInterval(min, max);
minMarkerValue = getMinMarkerValue(min, interval);
maxMarkerValue = minMarkerValue + interval * ChartView.Y_AXIS_INTERVALS;
}
/** /**
* Gets the largest marker. * Gets the interval value.
*/ *
public String getLargestMarker() { * @param min the min value
String minMarker = numberFormat.format(getMinMarkerValue()); * @param max the max value
String maxMarker = numberFormat.format(getMaxMarkerValue()); */
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker; private int getInterval(double min, double max) {
} for (int intervalValue : intervalValues) {
int minValue = getMinMarkerValue(min, intervalValue);
double targetInterval = (max - minValue) / ChartView.Y_AXIS_INTERVALS;
if (intervalValue >= targetInterval) {
return intervalValue;
}
}
// Return the largest interval
return intervalValues[intervalValues.length - 1];
}
/** /**
* Formats a marker value. * Gets the min marker value.
* *
* @param value the value * @param min the min series value
*/ * @param intervalValue the interval value
public String formatMarker(int value) { */
return numberFormat.format(value); private int getMinMarkerValue(double min, int intervalValue) {
} // Round down to the nearest intervalValue
int value = ((int) (min / intervalValue)) * intervalValue;
// value > min if min is negative
if (value > min) {
return value - intervalValue;
}
return value;
}
/**
* Gets the interval value.
*/
public int getInterval() {
return interval;
}
/**
* Gets the min marker value.
*/
public int getMinMarkerValue() {
return minMarkerValue;
}
/**
* Gets the max marker value.
*/
@VisibleForTesting
int getMaxMarkerValue() {
return maxMarkerValue;
}
/**
* Gets the title id.
*/
public int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId;
}
/**
* Gets the title paint.
*/
public Paint getTitlePaint() {
return titlePaint;
}
/**
* Gets the marker paint.
*/
public Paint getMarkerPaint() {
return markerPaint;
}
/**
* Gets the largest marker.
*/
public String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
}
/**
* Formats a marker value.
*
* @param value the value
*/
public String formatMarker(int value) {
return numberFormat.format(value);
}
} }
File diff suppressed because it is too large Load Diff
@@ -23,15 +23,15 @@ package com.google.android.apps.mytracks;
*/ */
public class Constants { public class Constants {
private Constants() {} /**
* Maximum number of waypoints that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* The settings file name.
*/
public static final String SETTINGS_NAME = "SettingsActivity";
/** private Constants() {
* Maximum number of waypoints that will be loaded at one time. }
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* The settings file name.
*/
public static final String SETTINGS_NAME = "SettingsActivity";
} }
@@ -25,22 +25,22 @@ import android.view.Menu;
*/ */
public interface ContextualActionModeCallback { public interface ContextualActionModeCallback {
/** /**
* Invoked to prepare the menu for the selected items. * Invoked to prepare the menu for the selected items.
* *
* @param menu the menu * @param menu the menu
* @param positions the selected items' positions * @param positions the selected items' positions
* @param ids the selected items' ids, if available * @param ids the selected items' ids, if available
* @param showSelectAll true to show select all * @param showSelectAll true to show select all
*/ */
void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll); void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll);
/** /**
* Invoked when items are selected. * Invoked when items are selected.
* *
* @param itemId the context menu item id * @param itemId the context menu item id
* @param positions the selected items' positions * @param positions the selected items' positions
* @param ids the selected items' ids, if available * @param ids the selected items' ids, if available
*/ */
boolean onClick(int itemId, int[] positions, long[] ids); boolean onClick(int itemId, int[] positions, long[] ids);
} }
@@ -16,15 +16,15 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
/** /**
* An activity for delete tracks. * An activity for delete tracks.
* *
@@ -32,66 +32,66 @@ import android.os.Bundle;
*/ */
public class DeleteActivity extends Activity { public class DeleteActivity extends Activity {
public static final String EXTRA_TRACK_IDS = "track_ids"; public static final String EXTRA_TRACK_IDS = "track_ids";
private static final int DIALOG_PROGRESS_ID = 0; private static final int DIALOG_PROGRESS_ID = 0;
private DeleteAsyncTask deleteAsyncTask; private DeleteAsyncTask deleteAsyncTask;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED); setResult(RESULT_CANCELED);
Intent intent = getIntent(); Intent intent = getIntent();
long[] trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS); long[] trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
Object retained = getLastNonConfigurationInstance(); Object retained = getLastNonConfigurationInstance();
if (retained instanceof DeleteAsyncTask) { if (retained instanceof DeleteAsyncTask) {
deleteAsyncTask = (DeleteAsyncTask) retained; deleteAsyncTask = (DeleteAsyncTask) retained;
deleteAsyncTask.setActivity(this); deleteAsyncTask.setActivity(this);
} else { } else {
deleteAsyncTask = new DeleteAsyncTask(this, trackIds); deleteAsyncTask = new DeleteAsyncTask(this, trackIds);
deleteAsyncTask.execute(); deleteAsyncTask.execute();
}
} }
}
@Override @Override
public Object onRetainNonConfigurationInstance() { public Object onRetainNonConfigurationInstance() {
deleteAsyncTask.setActivity(null); deleteAsyncTask.setActivity(null);
return deleteAsyncTask; return deleteAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
} }
return DialogUtils.createSpinnerProgressDialog(
this, R.string.track_delete_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
deleteAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
});
}
/** @Override
* Invokes when the associated AsyncTask completes. protected Dialog onCreateDialog(int id) {
*/ if (id != DIALOG_PROGRESS_ID) {
public void onAsyncTaskCompleted() { return null;
removeDialog(DIALOG_PROGRESS_ID); }
setResult(RESULT_OK); return DialogUtils.createSpinnerProgressDialog(
finish(); this, R.string.track_delete_progress_message, new DialogInterface.OnCancelListener() {
} @Override
public void onCancel(DialogInterface dialog) {
deleteAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
});
}
/** /**
* Shows the progress dialog. * Invokes when the associated AsyncTask completes.
*/ */
public void showProgressDialog() { public void onAsyncTaskCompleted() {
showDialog(DIALOG_PROGRESS_ID); removeDialog(DIALOG_PROGRESS_ID);
} setResult(RESULT_OK);
finish();
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
} }
@@ -28,68 +28,67 @@ import com.google.android.apps.mytracks.content.ContentProviderUtils;
*/ */
public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> { public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private DeleteActivity deleteActivity; private final long[] trackIds;
private final long[] trackIds; private final Context context;
private final Context context; private DeleteActivity deleteActivity;
// true if the AsyncTask has completed
private boolean completed;
// true if the AsyncTask has completed /**
private boolean completed; * Creates an AsyncTask.
*
/** * @param deleteActivity the activity currently associated with this task
* Creates an AsyncTask. * @param trackIds the track ids to delete. To delete all, set to size 1 with
* * trackIds[0] == -1L
* @param deleteActivity the activity currently associated with this task */
* @param trackIds the track ids to delete. To delete all, set to size 1 with public DeleteAsyncTask(DeleteActivity deleteActivity, long[] trackIds) {
* trackIds[0] == -1L this.deleteActivity = deleteActivity;
*/ this.trackIds = trackIds;
public DeleteAsyncTask(DeleteActivity deleteActivity, long[] trackIds) { context = deleteActivity.getApplicationContext();
this.deleteActivity = deleteActivity; completed = false;
this.trackIds = trackIds;
context = deleteActivity.getApplicationContext();
completed = false;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param deleteActivity the current activity, can be null
*/
public void setActivity(DeleteActivity deleteActivity) {
this.deleteActivity = deleteActivity;
if (completed && deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
} }
}
@Override /**
protected void onPreExecute() { * Sets the current activity associated with this AyncTask.
if (deleteActivity != null) { *
deleteActivity.showProgressDialog(); * @param deleteActivity the current activity, can be null
} */
} public void setActivity(DeleteActivity deleteActivity) {
this.deleteActivity = deleteActivity;
@Override if (completed && deleteActivity != null) {
protected Boolean doInBackground(Void... params) { deleteActivity.onAsyncTaskCompleted();
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context);
} else {
for (long id : trackIds) {
if (isCancelled()) {
return false;
} }
contentProviderUtils.deleteTrack(context, id);
}
} }
return true;
}
@Override @Override
protected void onPostExecute(Boolean result) { protected void onPreExecute() {
completed = true; if (deleteActivity != null) {
if (deleteActivity != null) { deleteActivity.showProgressDialog();
deleteActivity.onAsyncTaskCompleted(); }
}
@Override
protected Boolean doInBackground(Void... params) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context);
} else {
for (long id : trackIds) {
if (isCancelled()) {
return false;
}
contentProviderUtils.deleteTrack(context, id);
}
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
}
} }
}
} }
@@ -40,87 +40,87 @@ import java.util.ArrayList;
*/ */
public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller { public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerDetailActivity.class.getSimpleName(); private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private ArrayList<Long> markerIds; private ArrayList<Long> markerIds;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
long markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L); long markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId == -1L) { if (markerId == -1L) {
Log.d(TAG, "invalid marker id"); Log.d(TAG, "invalid marker id");
finish(); finish();
return; return;
}
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(this);
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
markerIds = new ArrayList<>();
int markerIndex = -1;
try (Cursor cursor = contentProviderUtils.getWaypointCursor(waypoint.getTrackId(), -1L, -1)) {
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
Waypoint current = contentProviderUtils.createWaypoint(cursor);
markerIds.add(current.getId());
if (current.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
}
}
}
ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
viewPager.setAdapter(new MarkerDetailPagerAdapter(getSupportFragmentManager()));
if (markerIndex != -1) {
viewPager.setCurrentItem(markerIndex);
}
} }
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(this); @Override
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId); protected int getLayoutResId() {
return R.layout.marker_detail_activity;
markerIds = new ArrayList<>();
int markerIndex = -1;
try (Cursor cursor = contentProviderUtils.getWaypointCursor(waypoint.getTrackId(), -1L, -1)) {
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
Waypoint current = contentProviderUtils.createWaypoint(cursor);
markerIds.add(current.getId());
if (current.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
}
}
}
ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
viewPager.setAdapter(new MarkerDetailPagerAdapter(getSupportFragmentManager()));
if (markerIndex != -1) {
viewPager.setCurrentItem(markerIndex);
} }
}
@Override @Override
protected int getLayoutResId() { public void onDeleteMarkerDone() {
return R.layout.marker_detail_activity; runOnUiThread(new Runnable() {
} @Override
public void run() {
finish();
}
});
}
/**
* Marker detail pager adapter.
*
* @author Jimmy Shih
*/
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public void onDeleteMarkerDone() {
runOnUiThread(new Runnable() {
@Override @Override
public void run() { public Fragment getItem(int position) {
finish(); String title = getString(R.string.marker_title, position + 1, getCount());
} return MarkerDetailFragment.newInstance(markerIds.get(position), title);
}); }
}
/** @Override
* Marker detail pager adapter. public int getCount() {
* return markerIds.size();
* @author Jimmy Shih }
*/
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
} }
@Override
public Fragment getItem(int position) {
String title = getString(R.string.marker_title, position + 1, getCount());
return MarkerDetailFragment.newInstance(markerIds.get(position), title);
}
@Override
public int getCount() {
return markerIds.size();
}
}
} }
@@ -16,14 +16,6 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@@ -33,6 +25,14 @@ import android.widget.AutoCompleteTextView;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
/** /**
* An activity to add/edit a marker. * An activity to add/edit a marker.
* *
@@ -40,152 +40,150 @@ import android.widget.EditText;
*/ */
public class MarkerEditActivity extends AbstractActivity { public class MarkerEditActivity extends AbstractActivity {
private static final String TAG = MarkerEditActivity.class.getSimpleName(); public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private long trackId;
private long markerId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Waypoint waypoint;
public static final String EXTRA_TRACK_ID = "track_id"; // UI elements
public static final String EXTRA_MARKER_ID = "marker_id"; private View statisticsSection;
private EditText statisticsName;
private View waypointSection;
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
private Button done;
private long trackId; @Override
private long markerId; protected void onCreate(Bundle bundle) {
private TrackRecordingServiceConnection trackRecordingServiceConnection; super.onCreate(bundle);
private Waypoint waypoint;
// UI elements trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
private View statisticsSection; markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
private EditText statisticsName; trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
private View waypointSection;
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
private Button done;
@Override // Setup UI elements
protected void onCreate(Bundle bundle) { statisticsSection = findViewById(R.id.marker_edit_statistics_section);
super.onCreate(bundle); statisticsName = findViewById(R.id.marker_edit_statistics_name);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L); waypointSection = findViewById(R.id.marker_edit_waypoint_section);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L); waypointName = findViewById(R.id.marker_edit_waypoint_name);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
// Setup UI elements Button cancel = findViewById(R.id.marker_edit_cancel);
statisticsSection = findViewById(R.id.marker_edit_statistics_section); cancel.setOnClickListener(new OnClickListener() {
statisticsName = findViewById(R.id.marker_edit_statistics_name); @Override
public void onClick(View v) {
finish();
}
});
done = findViewById(R.id.marker_edit_done);
updateUiByMarkerId();
}
waypointSection = findViewById(R.id.marker_edit_waypoint_section); @Override
waypointName = findViewById(R.id.marker_edit_waypoint_name); protected void onStart() {
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type); super.onStart();
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource( TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line); }
waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
Button cancel = findViewById(R.id.marker_edit_cancel); @Override
cancel.setOnClickListener(new OnClickListener() { protected void onStop() {
@Override super.onStop();
public void onClick(View v) { trackRecordingServiceConnection.unbind();
finish(); }
}
});
done = findViewById(R.id.marker_edit_done);
updateUiByMarkerId();
}
@Override @Override
protected void onStart() { protected int getLayoutResId() {
super.onStart(); return R.layout.marker_edit;
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); }
}
@Override /**
protected void onStop() { * Updates the UI based on the marker id.
super.onStop(); */
trackRecordingServiceConnection.unbind(); private void updateUiByMarkerId() {
} final boolean newMarker = markerId == -1L;
@Override setTitle(newMarker ? R.string.menu_insert_marker : R.string.menu_edit);
protected int getLayoutResId() { done.setText(newMarker ? R.string.generic_add : R.string.generic_save);
return R.layout.marker_edit; done.setOnClickListener(new View.OnClickListener() {
} @Override
public void onClick(View v) {
if (newMarker) {
addMarker();
} else {
saveMarker();
}
}
});
/**
* Updates the UI based on the marker id.
*/
private void updateUiByMarkerId() {
final boolean newMarker = markerId == -1L;
setTitle(newMarker ? R.string.menu_insert_marker : R.string.menu_edit);
done.setText(newMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (newMarker) { if (newMarker) {
addMarker(); statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1
: ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else { } else {
saveMarker(); waypoint = ContentProviderUtils.Factory.get(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
statisticsSection.setVisibility(statistics ? View.VISIBLE : View.GONE);
waypointSection.setVisibility(statistics ? View.GONE : View.VISIBLE);
if (statistics) {
statisticsName.setText(waypoint.getName());
} else {
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
}
} }
}
});
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1
: ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = ContentProviderUtils.Factory.get(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
statisticsSection.setVisibility(statistics ? View.VISIBLE : View.GONE);
waypointSection.setVisibility(statistics ? View.GONE : View.VISIBLE);
if (statistics) {
statisticsName.setText(waypoint.getName());
} else {
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
}
} }
}
/** /**
* Adds a marker. * Adds a marker.
*/ */
private void addMarker() { private void addMarker() {
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest( WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(
WaypointType.WAYPOINT, WaypointType.WAYPOINT,
false, false,
waypointName.getText().toString(), waypointName.getText().toString(),
waypointMarkerType.getText().toString(), waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(), waypointDescription.getText().toString(),
null, null,
null); null);
TrackRecordingServiceConnectionUtils.addMarker( TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, waypointCreationRequest); this, trackRecordingServiceConnection, waypointCreationRequest);
} }
/** /**
* Saves a marker. * Saves a marker.
*/ */
private void saveMarker() { private void saveMarker() {
boolean statistics = waypoint.getType() == WaypointType.STATISTICS; boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
if (statistics) { if (statistics) {
waypoint.setName(statisticsName.getText().toString()); waypoint.setName(statisticsName.getText().toString());
} else { } else {
waypoint.setName(waypointName.getText().toString()); waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString()); waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString()); waypoint.setDescription(waypointDescription.getText().toString());
}
ContentProviderUtils.Factory.get(this).updateWaypoint(waypoint);
} }
ContentProviderUtils.Factory.get(this).updateWaypoint(waypoint);
}
} }
@@ -58,279 +58,273 @@ import com.google.android.maps.mytracks.R;
*/ */
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller { public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
private static final String TAG = MarkerListActivity.class.getSimpleName(); private static final String TAG = MarkerListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[] { WaypointsColumns._ID, private static final String[] PROJECTION = new String[]{WaypointsColumns._ID,
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL, WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE}; WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE};
private ContentProviderUtils contentProviderUtils;
// Callback when an item is selected in the contextual action mode private SharedPreferences sharedPreferences;
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(MarkerListActivity.this, R.string.recording_track_id_key);
}
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(MarkerListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
MarkerListActivity.this.invalidateOptionsMenu();
}
});
}
}
};
private Track track;
private ResourceCursorAdapter resourceCursorAdapter;
// UI elements
private ListView listView;
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override @Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) { public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isSingleSelection = ids.length == 1; boolean isSingleSelection = ids.length == 1;
menu.findItem(R.id.list_context_menu_share).setVisible(false); menu.findItem(R.id.list_context_menu_share).setVisible(false);
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_delete).setVisible(true); menu.findItem(R.id.list_context_menu_delete).setVisible(true);
/* /*
* Set select all to the same visibility as delete since delete is the * Set select all to the same visibility as delete since delete is the
* only action that can be applied to multiple markers. * only action that can be applied to multiple markers.
*/ */
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll); menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
} }
@Override @Override
public boolean onClick(int itemId, int[] positions, long[] ids) { public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, ids); return handleContextItem(itemId, ids);
} }
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(MarkerListActivity.this, R.string.recording_track_id_key);
}
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(MarkerListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
MarkerListActivity.this.invalidateOptionsMenu();
}
});
}
}
};
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private Track track;
private ResourceCursorAdapter resourceCursorAdapter;
// UI elements
private ListView listView;
private MenuItem insertMarkerMenuItem;
private MenuItem searchMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
track = trackId != -1L ? contentProviderUtils.getTrack(trackId) : null;
final long trackFirstWaypointId = trackId != -1 ? contentProviderUtils.getFirstWaypointId(trackId) : -1;
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
listView = findViewById(R.id.marker_list);
listView.setEmptyView(findViewById(R.id.marker_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
startActivity(intent);
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(WaypointsColumns.PHOTOURL);
int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE);
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)] == WaypointType.STATISTICS;
int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = statistics ? null : cursor.getString(categoryIndex);
String description = statistics ? null : cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
}
}; };
listView.setAdapter(resourceCursorAdapter); private MenuItem insertMarkerMenuItem;
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback); private MenuItem searchMenuItem;
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() { @Override
@NonNull protected void onCreate(Bundle savedInstanceState) {
@Override super.onCreate(savedInstanceState);
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
if (track != null) {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + "!=?",
new String[] { String.valueOf(track.getId()), String.valueOf(trackFirstWaypointId) }, null);
} else {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.STARTTIME + " IS NULL",null, null);
}
}
@Override contentProviderUtils = ContentProviderUtils.Factory.get(this);
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) { sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
resourceCursorAdapter.swapCursor(cursor);
}
@Override long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
public void onLoaderReset(@NonNull Loader<Cursor> loader) { track = trackId != -1L ? contentProviderUtils.getTrack(trackId) : null;
resourceCursorAdapter.swapCursor(null); final long trackFirstWaypointId = trackId != -1 ? contentProviderUtils.getFirstWaypointId(trackId) : -1;
}
});
}
@Override setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
@Override listView = findViewById(R.id.marker_list);
protected void onResume() { listView.setEmptyView(findViewById(R.id.marker_list_empty));
super.onResume(); listView.setOnItemClickListener(new OnItemClickListener() {
this.invalidateOptionsMenu(); @Override
} public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
startActivity(intent);
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(WaypointsColumns.PHOTOURL);
int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE);
@Override boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)] == WaypointType.STATISTICS;
protected void onStop() { int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
super.onStop(); String name = cursor.getString(nameIndex);
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); long time = cursor.getLong(timeIndex);
} String category = statistics ? null : cursor.getString(categoryIndex);
String description = statistics ? null : cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
@Override ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
protected int getLayoutResId() { }
return R.layout.marker_list; };
} listView.setAdapter(resourceCursorAdapter);
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
@Override LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
public boolean onCreateOptionsMenu(Menu menu) { @NonNull
getMenuInflater().inflate(R.menu.marker_list, menu); @Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
if (track != null) {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + "!=?",
new String[]{String.valueOf(track.getId()), String.valueOf(trackFirstWaypointId)}, null);
} else {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.STARTTIME + " IS NULL", null, null);
}
}
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker); @Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
searchMenuItem = menu.findItem(R.id.marker_list_search); @Override
AbstractTrackActivity.configureSearchWidget(this, searchMenuItem, null); public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
return super.onCreateOptionsMenu(menu); }
} });
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
insertMarkerMenuItem.setVisible(track != null && track.getId() == recordingTrackId && !recordingTrackPaused);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
startActivity(intent);
return true;
} }
return super.onOptionsItemSelected(item);
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { protected void onStart() {
super.onCreateContextMenu(menu, v, menuInfo); super.onStart();
getMenuInflater().inflate(R.menu.list_context_menu, menu); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[] { info.position }, new long[] { info.id }, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[] {info.id})) {
return true;
} }
return super.onContextItemSelected(item);
}
/** @Override
* Handles a context item selection. protected void onResume() {
* super.onResume();
* @param itemId the menu item id this.invalidateOptionsMenu();
* @param markerIds the marker ids
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long[] markerIds) {
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerIds[0]);
//TODO Use IntentUtils.newShowOnMapIntent()
startActivity(intent);
}
return true;
case R.id.list_context_menu_edit:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
}
return true;
case R.id.list_context_menu_delete:
if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
markerIds = new long[] { -1L };
}
DeleteMarkerDialogFragment.newInstance(markerIds)
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
listView.setItemChecked(i, true);
}
return false;
default:
return false;
} }
}
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { protected void onStop() {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) { super.onStop();
return true; sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
return super.onKeyUp(keyCode, event);
}
@Override @Override
public void onDeleteMarkerDone() { protected int getLayoutResId() {
// Do nothing return R.layout.marker_list;
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.marker_list, menu);
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search);
AbstractTrackActivity.configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
insertMarkerMenuItem.setVisible(track != null && track.getId() == recordingTrackId && !recordingTrackPaused);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
return true;
}
return super.onContextItemSelected(item);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param markerIds the marker ids
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long[] markerIds) {
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerIds[0]);
//TODO Use IntentUtils.newShowOnMapIntent()
startActivity(intent);
}
return true;
case R.id.list_context_menu_edit:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
}
return true;
case R.id.list_context_menu_delete:
if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
markerIds = new long[]{-1L};
}
DeleteMarkerDialogFragment.newInstance(markerIds)
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
listView.setItemChecked(i, true);
}
return false;
default:
return false;
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onDeleteMarkerDone() {
// Do nothing
}
} }
@@ -70,444 +70,441 @@ import java.util.SortedSet;
* An activity to display a list of search results. * An activity to display a list of search results.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
* * <p>
* TODO: allow to refine search (present search in context menu) * TODO: allow to refine search (present search in context menu)
*/ */
public class SearchListActivity extends AbstractTrackActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller { public class SearchListActivity extends AbstractTrackActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = SearchListActivity.class.getSimpleName(); private static final String TAG = SearchListActivity.class.getSimpleName();
private static final String IS_RECORDING_FIELD = "isRecording"; private static final String IS_RECORDING_FIELD = "isRecording";
private static final String IS_PAUSED_FIELD = "isPaused"; private static final String IS_PAUSED_FIELD = "isPaused";
private static final String ICON_ID_FIELD = "icon"; private static final String ICON_ID_FIELD = "icon";
private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription"; private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription";
private static final String NAME_FIELD = "name"; private static final String NAME_FIELD = "name";
private static final String TOTAL_TIME_FIELD = "totalTime"; private static final String TOTAL_TIME_FIELD = "totalTime";
private static final String TOTAL_DISTANCE_FIELD = "totalDistance"; private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
private static final String MARKER_COUNT_FIELD = "markerCount"; private static final String MARKER_COUNT_FIELD = "markerCount";
private static final String START_TIME_FIELD = "startTime"; private static final String START_TIME_FIELD = "startTime";
private static final String CATEGORY_FIELD = "category"; private static final String CATEGORY_FIELD = "category";
private static final String DESCRIPTION_FIELD = "description"; private static final String DESCRIPTION_FIELD = "description";
private static final String PHOTO_URL_FIELD = "photoUrl"; private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String MARKER_LATITUDE_FIELD = "latitude"; private static final String MARKER_LATITUDE_FIELD = "latitude";
private static final String MARKER_LONGITUDE_FIELD = "longitude"; private static final String MARKER_LONGITUDE_FIELD = "longitude";
private static final String TRACK_ID_FIELD = "trackId"; private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId"; private static final String MARKER_ID_FIELD = "markerId";
private ContentProviderUtils contentProviderUtils;
private final OnSharedPreferenceChangeListener private SharedPreferences sharedPreferences;
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private TrackRecordingServiceConnection trackRecordingServiceConnection;
@Override private SearchEngine searchEngine;
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { private SearchRecentSuggestions searchRecentSuggestions;
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) { private ArrayAdapter<Map<String, Object>> arrayAdapter;
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this); private boolean metricUnits = true;
} private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key))) { // Callback when an item is selected in the contextual action mode
recordingTrackId = PreferencesUtils.getLong(SearchListActivity.this, R.string.recording_track_id_key); private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
}
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(SearchListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
arrayAdapter.notifyDataSetChanged();
}
});
}
}
};
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isSingleSelection = positions.length == 1;
boolean isSingleSelectionTrack;
if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Track track = contentProviderUtils.getTrack(trackId);
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else {
isSingleSelectionTrack = false;
}
// Not recording, one item, item is a track
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack);
//TODO Setup shareIntent.
// One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
// One item. If track, no restriction.
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && (isSingleSelectionTrack));
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
};
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine;
private SearchRecentSuggestions searchRecentSuggestions;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils);
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@NonNull
@Override @Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) { public boolean onClick(int itemId, int[] positions, long[] ids) {
View view; return handleContextItem(itemId, positions);
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
} }
Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
int iconId = (int) resultMap.get(ICON_ID_FIELD);
int iconContentDescriptionId = (int) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD);
String name = (String) resultMap.get(NAME_FIELD);
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
int markerCount = (int) resultMap.get(MARKER_COUNT_FIELD);
long startTime = (long) resultMap.get(START_TIME_FIELD);
String category = (String) resultMap.get(CATEGORY_FIELD);
String description = (String) resultMap.get(DESCRIPTION_FIELD);
String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD);
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId, @Override
iconContentDescriptionId, name, totalTime, totalDistance, markerCount, public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
startTime, false, category, description, photoUrl); boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
return view; boolean isSingleSelection = positions.length == 1;
} boolean isSingleSelectionTrack;
if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Track track = contentProviderUtils.getTrack(trackId);
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else {
isSingleSelectionTrack = false;
}
// Not recording, one item, item is a track
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack);
//TODO Setup shareIntent.
// One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
// One item. If track, no restriction.
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && (isSingleSelectionTrack));
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
}; };
// UI elements private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
ListView listView = findViewById(R.id.search_list); private final OnSharedPreferenceChangeListener
listView.setAdapter(arrayAdapter); sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
listView.setEmptyView(findViewById(R.id.search_list_empty)); @Override
listView.setOnItemClickListener(new OnItemClickListener() { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
@Override if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
Map<String, Object> item = arrayAdapter.getItem(position); }
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(SearchListActivity.this, R.string.recording_track_id_key);
}
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(SearchListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
arrayAdapter.notifyDataSetChanged();
}
});
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils);
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
View view;
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
}
Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
int iconId = (int) resultMap.get(ICON_ID_FIELD);
int iconContentDescriptionId = (int) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD);
String name = (String) resultMap.get(NAME_FIELD);
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
int markerCount = (int) resultMap.get(MARKER_COUNT_FIELD);
long startTime = (long) resultMap.get(START_TIME_FIELD);
String category = (String) resultMap.get(CATEGORY_FIELD);
String description = (String) resultMap.get(DESCRIPTION_FIELD);
String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD);
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId,
iconContentDescriptionId, name, totalTime, totalDistance, markerCount,
startTime, false, category, description, photoUrl);
return view;
}
};
// UI elements
ListView listView = findViewById(R.id.search_list);
listView.setAdapter(arrayAdapter);
listView.setEmptyView(findViewById(R.id.search_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
if (markerId != null) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
}
});
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
}
@Override
protected void onResume() {
super.onResume();
arrayAdapter.notifyDataSetChanged();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
}
@Override
protected int getLayoutResId() {
return R.layout.search_list;
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_list, menu);
menu.findItem(R.id.search_list_search).setVisible(false);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.search_list_search) {
return false;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new int[]{info.position})) {
return true;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param positions the positions of the selected rows
* @return true if handled.
*/
private boolean handleContextItem(int itemId, int[] positions) {
if (positions.length != 1) {
return false;
}
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD); Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD); Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent; Intent intent;
if (markerId != null) { switch (itemId) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class) case R.id.list_context_menu_share:
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId); //TODO
} else { Log.e(TAG, "Not implemented");
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class) return true;
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newShowOnMapIntent((double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
// Close the search result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (markerId != null) {
DeleteMarkerDialogFragment.newInstance(new long[]{markerId}).show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
} else {
deleteTracks(new long[]{trackId});
}
return true;
default:
return false;
} }
startActivity(intent);
}
});
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
}
@Override
protected void onResume() {
super.onResume();
arrayAdapter.notifyDataSetChanged();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
}
@Override
protected int getLayoutResId() {
return R.layout.search_list;
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_list, menu);
menu.findItem(R.id.search_list_search).setVisible(false);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.search_list_search) {
return false;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[] { info.position }, new long[] { info.id }, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new int[] {info.position})) {
return true;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param positions the positions of the selected rows
* @return true if handled.
*/
private boolean handleContextItem(int itemId, int[] positions) {
if (positions.length != 1) {
return false;
}
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
switch (itemId) {
case R.id.list_context_menu_share:
//TODO
Log.e(TAG, "Not implemented");
return true;
case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newShowOnMapIntent((double)item.get(MARKER_LATITUDE_FIELD), (double)item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
// Close the search result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (markerId != null) {
DeleteMarkerDialogFragment.newInstance(new long[] { markerId }).show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
} else {
deleteTracks(new long[] { trackId });
}
return true;
default:
return false;
}
}
/**
* Handles the intent.
*
* @param intent the intent
*/
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Invalid intent action: " + intent);
finish();
return;
} }
final String textQuery = intent.getStringExtra(SearchManager.QUERY); /**
setTitle(textQuery); * Handles the intent.
*
doSearch(textQuery); * @param intent the intent
}
/**
* Do the search.
*
* @param textQuery the query
*/
private void doSearch(String textQuery) {
SearchQuery query = new SearchQuery(textQuery, null, -1L, System.currentTimeMillis());
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
// Use the UI thread to display the results
runOnUiThread(new Runnable() {
@Override
public void run() {
arrayAdapter.clear();
arrayAdapter.addAll(displayResults);
}
});
// Save the query as a suggestion for the future
searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
}
/**
* Prepares the result for display.
*
* @param scoredResults a list of score results
* @return a list of result maps
*/
private List<Map<String, Object>> prepareResultsforDisplay(Collection<ScoredResult> scoredResults) {
//TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<>();
if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap);
} else {
prepareMarkerForDisplay(result.waypoint, resultMap);
}
output.add(resultMap);
}
return output;
}
/**
* Prepares a marker for display by filling in a result map.
*
* @param waypoint the marker
* @param resultMap the result map
*/
@Deprecated
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
/*
* TODO: It may be more appropriate to obtain the track name as a join in
* the retrieval phase of the search.
*/ */
String trackName = null; private void handleIntent(Intent intent) {
long trackId = waypoint.getTrackId(); if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
if (trackId != -1L) { Log.e(TAG, "Invalid intent action: " + intent);
Track track = contentProviderUtils.getTrack(trackId); finish();
if (track != null) { return;
trackName = track.getName(); }
}
final String textQuery = intent.getStringExtra(SearchManager.QUERY);
setTitle(textQuery);
doSearch(textQuery);
} }
boolean statistics = waypoint.getType() == WaypointType.STATISTICS; /**
* Do the search.
*
* @param textQuery the query
*/
private void doSearch(String textQuery) {
SearchQuery query = new SearchQuery(textQuery, null, -1L, System.currentTimeMillis());
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
resultMap.put(IS_RECORDING_FIELD, false); // Use the UI thread to display the results
resultMap.put(IS_PAUSED_FIELD, true); runOnUiThread(new Runnable() {
resultMap.put(ICON_ID_FIELD, statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin); @Override
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker); public void run() {
resultMap.put(NAME_FIELD, waypoint.getName()); arrayAdapter.clear();
// Display the marker's track name in the total time field arrayAdapter.addAll(displayResults);
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName)); }
resultMap.put(TOTAL_DISTANCE_FIELD, null); });
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude()); // Save the query as a suggestion for the future
resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude()); searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
} }
/** /**
* Prepares a track for display by filling in a result map. * Prepares the result for display.
* *
* @param track the track * @param scoredResults a list of score results
* @param resultMap the result map * @return a list of result maps
*/ */
@Deprecated private List<Map<String, Object>> prepareResultsforDisplay(Collection<ScoredResult> scoredResults) {
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) { //TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly.
TripStatistics tripStatitics = track.getTripStatistics(); ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
String icon = track.getIcon(); for (ScoredResult result : scoredResults) {
String category = icon != null && !icon.equals("") ? null : track.getCategory(); Map<String, Object> resultMap = new HashMap<>();
if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap);
} else {
prepareMarkerForDisplay(result.waypoint, resultMap);
}
output.add(resultMap);
}
return output;
}
resultMap.put(IS_RECORDING_FIELD, track.getId() == recordingTrackId); /**
resultMap.put(IS_PAUSED_FIELD, recordingTrackPaused); * Prepares a marker for display by filling in a result map.
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon)); *
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track); * @param waypoint the marker
resultMap.put(NAME_FIELD, track.getName()); * @param resultMap the result map
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime())); */
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits)); @Deprecated
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId())); private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime()); /*
resultMap.put(CATEGORY_FIELD, category); * TODO: It may be more appropriate to obtain the track name as a join in
resultMap.put(DESCRIPTION_FIELD, track.getDescription()); * the retrieval phase of the search.
resultMap.put(PHOTO_URL_FIELD, null); */
resultMap.put(TRACK_ID_FIELD, track.getId()); String trackName = null;
resultMap.put(MARKER_ID_FIELD, null); long trackId = waypoint.getTrackId();
} if (trackId != -1L) {
Track track = contentProviderUtils.getTrack(trackId);
if (track != null) {
trackName = track.getName();
}
}
@Override boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
public void onDeleteMarkerDone() {
runOnUiThread(new Runnable() {
@Override
public void run() {
handleIntent(getIntent());
}
});
}
@Override resultMap.put(IS_RECORDING_FIELD, false);
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() { resultMap.put(IS_PAUSED_FIELD, true);
return trackRecordingServiceConnection; resultMap.put(ICON_ID_FIELD, statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin);
} resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName());
// Display the marker's track name in the total time field
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName));
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
@Override resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude());
protected void onDeleted() { resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude());
runOnUiThread(new Runnable() { }
@Override
public void run() { /**
handleIntent(getIntent()); * Prepares a track for display by filling in a result map.
} *
}); * @param track the track
} * @param resultMap the result map
*/
@Deprecated
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
TripStatistics tripStatitics = track.getTripStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
resultMap.put(IS_RECORDING_FIELD, track.getId() == recordingTrackId);
resultMap.put(IS_PAUSED_FIELD, recordingTrackPaused);
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null);
}
@Override
public void onDeleteMarkerDone() {
runOnUiThread(new Runnable() {
@Override
public void run() {
handleIntent(getIntent());
}
});
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(new Runnable() {
@Override
public void run() {
handleIntent(getIntent());
}
});
}
} }
@@ -18,16 +18,17 @@ package com.google.android.apps.mytracks;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TabHost; import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory; import android.widget.TabHost.TabContentFactory;
import android.widget.TabWidget; import android.widget.TabWidget;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
@@ -47,104 +48,106 @@ import java.util.ArrayList;
//TODO Rework or better remove. //TODO Rework or better remove.
public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener { public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
private final Context context;
private final TabHost tabHost;
private final ViewPager viewPager;
private final ArrayList<TabInfo> tabInfos = new ArrayList<>();
/**
* An object to hold a tab's info.
*
* @author Jimmy Shih
*/
private static final class TabInfo {
private final Class<?> clss;
private final Bundle bundle;
public TabInfo(Class<?> clss, Bundle bunlde) {
this.clss = clss;
this.bundle = bunlde;
}
}
/**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the
* {@link TabHost} API.
*
* @author Jimmy Shih
*/
private static class DummyTabFactory implements TabHost.TabContentFactory {
private final Context context; private final Context context;
private final TabHost tabHost;
private final ViewPager viewPager;
private final ArrayList<TabInfo> tabInfos = new ArrayList<>();
public DummyTabFactory(Context context) { public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager viewPager) {
this.context = context; super(activity.getSupportFragmentManager());
this.context = activity;
this.tabHost = tabHost;
this.viewPager = viewPager;
this.tabHost.setOnTabChangedListener(this);
this.viewPager.setAdapter(this);
this.viewPager.setOnPageChangeListener(this);
}
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle bundle) {
tabSpec.setContent(new DummyTabFactory(context));
TabInfo info = new TabInfo(clss, bundle);
tabInfos.add(info);
tabHost.addTab(tabSpec);
notifyDataSetChanged();
} }
@Override @Override
public View createTabContent(String tag) { public int getCount() {
View view = new View(context); return tabInfos.size();
view.setMinimumWidth(0);
view.setMinimumHeight(0);
return view;
} }
}
public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager viewPager) { @Override
super(activity.getSupportFragmentManager()); public Fragment getItem(int position) {
this.context = activity; TabInfo info = tabInfos.get(position);
this.tabHost = tabHost; return Fragment.instantiate(context, info.clss.getName(), info.bundle);
this.viewPager = viewPager; }
this.tabHost.setOnTabChangedListener(this);
this.viewPager.setAdapter(this);
this.viewPager.setOnPageChangeListener(this);
}
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle bundle) { @Override
tabSpec.setContent(new DummyTabFactory(context)); public void onTabChanged(String tabId) {
int position = tabHost.getCurrentTab();
viewPager.setCurrentItem(position);
}
TabInfo info = new TabInfo(clss, bundle); @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
tabInfos.add(info); @Override
tabHost.addTab(tabSpec); public void onPageSelected(int position) {
notifyDataSetChanged(); /*
} * Unfortunately when TabHost changes the current tab, it kindly also takes
* care of putting focus on it when not in touch mode. The jerk. This hack
* tries to prevent this from pulling focus out of our ViewPager.
*/
TabWidget tabWidget = tabHost.getTabWidget();
int oldFocusability = tabWidget.getDescendantFocusability();
tabWidget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
tabHost.setCurrentTab(position);
tabWidget.setDescendantFocusability(oldFocusability);
}
@Override @Override
public int getCount() { public void onPageScrollStateChanged(int state) {
return tabInfos.size(); }
}
@Override /**
public Fragment getItem(int position) { * An object to hold a tab's info.
TabInfo info = tabInfos.get(position); *
return Fragment.instantiate(context, info.clss.getName(), info.bundle); * @author Jimmy Shih
}
@Override
public void onTabChanged(String tabId) {
int position = tabHost.getCurrentTab();
viewPager.setCurrentItem(position);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
/*
* Unfortunately when TabHost changes the current tab, it kindly also takes
* care of putting focus on it when not in touch mode. The jerk. This hack
* tries to prevent this from pulling focus out of our ViewPager.
*/ */
TabWidget tabWidget = tabHost.getTabWidget(); private static final class TabInfo {
int oldFocusability = tabWidget.getDescendantFocusability();
tabWidget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
tabHost.setCurrentTab(position);
tabWidget.setDescendantFocusability(oldFocusability);
}
@Override private final Class<?> clss;
public void onPageScrollStateChanged(int state) {} private final Bundle bundle;
public TabInfo(Class<?> clss, Bundle bunlde) {
this.clss = clss;
this.bundle = bunlde;
}
}
/**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the
* {@link TabHost} API.
*
* @author Jimmy Shih
*/
private static class DummyTabFactory implements TabHost.TabContentFactory {
private final Context context;
public DummyTabFactory(Context context) {
this.context = context;
}
@Override
public View createTabContent(String tag) {
View view = new View(context);
view.setMinimumWidth(0);
view.setMinimumHeight(0);
return view;
}
}
} }
@@ -36,114 +36,114 @@ import com.google.android.maps.mytracks.R;
*/ */
public class TrackController { public class TrackController {
private static final String TAG = TrackController.class.getSimpleName(); private static final String TAG = TrackController.class.getSimpleName();
private final Activity activity; private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection; private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handlerUpdateTotalTime; private final Handler handlerUpdateTotalTime;
private final View containerView; private final View containerView;
private final TextView statusTextView; private final TextView statusTextView;
private final TextView totalTimeTextView; private final TextView totalTimeTextView;
private final ImageButton recordImageButton; private final ImageButton recordImageButton;
private final ImageButton stopImageButton; private final ImageButton stopImageButton;
private final boolean alwaysShow; private final boolean alwaysShow;
private boolean isRecording; private boolean isRecording;
private boolean isPaused; private boolean isPaused;
private long totalTime = 0; private long totalTime = 0;
// the timestamp for the toal time // the timestamp for the toal time
private long totalTimeTimestamp = 0; private long totalTimeTimestamp = 0;
private boolean isResumed = false; private boolean isResumed = false;
// A runnable to update the total time. // A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() { private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() { public void run() {
if (isResumed && isRecording && !isPaused) { if (isResumed && isRecording && !isPaused) {
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime)); totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND); handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND);
} }
} }
}; };
public TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) { public TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
this.activity = activity; this.activity = activity;
this.trackRecordingServiceConnection = trackRecordingServiceConnection; this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow; this.alwaysShow = alwaysShow;
handlerUpdateTotalTime = new Handler(); handlerUpdateTotalTime = new Handler();
containerView = activity.findViewById(R.id.track_controller_container); containerView = activity.findViewById(R.id.track_controller_container);
statusTextView = activity.findViewById(R.id.track_controller_status); statusTextView = activity.findViewById(R.id.track_controller_status);
totalTimeTextView = activity.findViewById(R.id.track_controller_total_time); totalTimeTextView = activity.findViewById(R.id.track_controller_total_time);
recordImageButton = activity.findViewById(R.id.track_controller_record); recordImageButton = activity.findViewById(R.id.track_controller_record);
recordImageButton.setOnClickListener(recordListener); recordImageButton.setOnClickListener(recordListener);
stopImageButton = activity.findViewById(R.id.track_controller_stop); stopImageButton = activity.findViewById(R.id.track_controller_stop);
stopImageButton.setOnClickListener(stopListener); stopImageButton.setOnClickListener(stopListener);
}
public void update(boolean recording, boolean paused) {
if (!isResumed) {
return;
}
isRecording = recording;
isPaused = paused;
boolean visible = alwaysShow || isRecording;
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
if (!visible) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
} }
recordImageButton.setImageResource(isRecording && !isPaused ? R.drawable.button_pause : R.drawable.button_record); public void update(boolean recording, boolean paused) {
recordImageButton.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record)); if (!isResumed) {
return;
}
isRecording = recording;
isPaused = paused;
boolean visible = alwaysShow || isRecording;
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
stopImageButton.setImageResource(isRecording ? R.drawable.button_stop : R.drawable.ic_button_stop_disabled); if (!visible) {
stopImageButton.setEnabled(isRecording); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE); recordImageButton.setImageResource(isRecording && !isPaused ? R.drawable.button_pause : R.drawable.button_record);
if (isRecording) { recordImageButton.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
statusTextView.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
statusTextView.setText(isPaused ? R.string.generic_paused : R.string.generic_recording); stopImageButton.setImageResource(isRecording ? R.drawable.button_stop : R.drawable.ic_button_stop_disabled);
stopImageButton.setEnabled(isRecording);
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
statusTextView.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
statusTextView.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
}
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
totalTimeTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
totalTime = getTotalTime();
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
if (!isPaused) {
totalTimeTimestamp = System.currentTimeMillis();
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND);
}
}
} }
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); public void onResume(boolean recording, boolean paused) {
totalTimeTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE); isResumed = true;
if (isRecording) { update(recording, paused);
totalTime = getTotalTime();
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
if (!isPaused) {
totalTimeTimestamp = System.currentTimeMillis();
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND);
}
} }
}
public void onResume(boolean recording, boolean paused) { public void onPause() {
isResumed = true; isResumed = false;
update(recording, paused); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
} }
public void onPause() { public void hide() {
isResumed = false; containerView.setVisibility(View.GONE);
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); }
}
public void hide() { public void show() {
containerView.setVisibility(View.GONE); containerView.setVisibility(View.VISIBLE);
} }
public void show() { /**
containerView.setVisibility(View.VISIBLE); * Gets the total time for the current recording track.
} */
private long getTotalTime() {
/** ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
* Gets the total time for the current recording track. return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
*/ }
private long getTotalTime() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
}
} }
@@ -76,445 +76,439 @@ import java.util.Date;
*/ */
public class TrackDetailActivity extends AbstractTrackActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller { public class TrackDetailActivity extends AbstractTrackActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackDetailActivity.class.getSimpleName(); public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
private static final String PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key";
private static final String JPEG_EXTENSION = "jpeg";
public static final String EXTRA_TRACK_ID = "track_id"; private static final int CAMERA_REQUEST_CODE = 5;
public static final String EXTRA_MARKER_ID = "marker_id"; private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; // The following are set in onCreate
private static final String PHOTO_URI_KEY = "photo_uri_key"; private boolean hasCamera;
private static final String HAS_PHOTO_KEY = "has_photo_key"; private Uri photoUri;
private static final String JPEG_EXTENSION = "jpeg"; private boolean hasPhoto;
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private TabHost tabHost;
private TrackController trackController;
private static final int CAMERA_REQUEST_CODE = 5; // From intent
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6; private long trackId;
// The following are set in onCreate // Preferences
private boolean hasCamera; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private Uri photoUri; private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean hasPhoto; private final Runnable bindChangedCallback = new Runnable() {
private ContentProviderUtils contentProviderUtils; @Override
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private TabHost tabHost;
private TrackController trackController;
// From intent
private long trackId;
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem;
private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
// After binding changes (is available), update the total time in
// trackController.
runOnUiThread(new Runnable() {
@Override
public void run() { public void run() {
trackController.update(isRecording(), recordingTrackPaused); // After binding changes (is available), update the total time in
if (hasPhoto && photoUri != null) { // trackController.
hasPhoto = false; runOnUiThread(new Runnable() {
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString()); @Override
long markerId = TrackRecordingServiceConnectionUtils.addMarker(TrackDetailActivity.this, trackRecordingServiceConnection, waypointCreationRequest); public void run() {
if (markerId != -1L) { trackController.update(isRecording(), recordingTrackPaused);
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri); if (hasPhoto && photoUri != null) {
} hasPhoto = false;
} WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString());
long markerId = TrackRecordingServiceConnectionUtils.addMarker(TrackDetailActivity.this, trackRecordingServiceConnection, waypointCreationRequest);
if (markerId != -1L) {
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
}
}
}
});
} }
}); };
} /*
}; * Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
/* */
* Note that sharedPreferenceChangeListener cannot be an anonymous inner private final OnSharedPreferenceChangeListener
* class. Anonymous inner class will get garbage collected. sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
*/ @Override
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key))) { if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(TrackDetailActivity.this, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getLong(TrackDetailActivity.this, R.string.recording_track_id_key);
} }
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_paused_key))) { if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
} }
if (key == null) return; if (key == null) return;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override
public void run() {
TrackDetailActivity.this.invalidateOptionsMenu();
boolean isRecording = isRecording();
trackController.update(isRecording, recordingTrackPaused);
}
});
}
};
private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem;
private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private final OnClickListener recordListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
trackController.update(true, true);
}
}
};
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
TrackRecordingServiceConnectionUtils.stopRecording(TrackDetailActivity.this, trackRecordingServiceConnection, true);
updateMenuItems(true);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
handleIntent(getIntent());
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
tabHost = findViewById(android.R.id.tabhost);
tabHost.setup();
ViewPager viewPager = findViewById(R.id.pager);
TabsAdapter tabsAdapter = new TabsAdapter(this, tabHost, viewPager);
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab), getResources().getDrawable(R.drawable.ic_tab_stats));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab), getResources().getDrawable(R.drawable.ic_tab_chart));
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
}
// Set the background after all three tabs are added
for (int i = 0; i < tabHost.getTabWidget().getChildCount(); i++) {
tabHost.getTabWidget().getChildAt(i).setBackgroundResource(R.drawable.tab_indicator_mytracks);
}
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackDataHub.start();
}
@Override
protected void onResume() {
super.onResume();
trackDataHub.loadTrack(trackId);
// Update UI
this.invalidateOptionsMenu();
trackController.onResume(isRecording(), recordingTrackPaused);
}
@Override
protected void onPause() {
super.onPause();
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
trackDataHub.stop();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
if (photoUri != null) {
outState.putParcelable(PHOTO_URI_KEY, photoUri);
}
outState.putBoolean(HAS_PHOTO_KEY, hasPhoto);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
return;
}
hasPhoto = resultCode == RESULT_OK;
if (hasPhoto) {
//Register photo in media scanner
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(photoUri);
this.sendBroadcast(mediaScanIntent);
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected int getLayoutResId() {
return R.layout.track_detail;
}
@Override
protected void onHomeSelected() {
/*
* TODO: Investigate
* According to
* http://developer.android.com/training/implementing-navigation
* /ancestral.html, we should use NavUtils.shouldUpRecreateTask instead of
* always creating a new back stack. However, NavUtils.shouldUpRecreateTask
* seems to always return false.
*/
TaskStackBuilder.create(this).addParentStack(TrackDetailActivity.class).startActivities();
finish();
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_detail, menu);
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
shareMenuItem = menu.findItem(R.id.track_detail_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
//TODO: Share the actual track when track is finished? How to get the file path or create a new file?
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, trackId, "", TrackFileFormat.KML));
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(recordingTrackPaused);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_insert_marker:
intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
return false;
}
takePicture();
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_voice_frequency:
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true;
case R.id.track_detail_split_frequency:
FrequencyDialogFragment.newInstance(R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_delete:
deleteTracks(new long[]{trackId});
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void takePicture() {
File dir = FileUtils.getPhotoDir(trackId);
FileUtils.ensureDirectoryExists(dir);
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
if (file != null) {
photoUri = FileProvider.getUriForFile(this, FileUtils.FILEPROVIDER, file);
Log.d(TAG, "Taking photo to URI: " + photoUri);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, CAMERA_REQUEST_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
takePicture();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
TrackDetailActivity.this.invalidateOptionsMenu(); finish();
boolean isRecording = isRecording();
trackController.update(isRecording, recordingTrackPaused);
} }
}); });
}
};
private final OnClickListener recordListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
trackController.update(true, true);
}
}
};
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
TrackRecordingServiceConnectionUtils.stopRecording(TrackDetailActivity.this, trackRecordingServiceConnection, true);
updateMenuItems(true);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
handleIntent(getIntent());
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
tabHost = findViewById(android.R.id.tabhost);
tabHost.setup();
ViewPager viewPager = findViewById(R.id.pager);
TabsAdapter tabsAdapter = new TabsAdapter(this, tabHost, viewPager);
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab), getResources().getDrawable(R.drawable.ic_tab_stats));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab),getResources().getDrawable(R.drawable.ic_tab_chart));
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
} }
// Set the background after all three tabs are added /**
for (int i = 0; i < tabHost.getTabWidget().getChildCount(); i++) { * Gets the {@link TrackDataHub}.
tabHost.getTabWidget().getChildAt(i).setBackgroundResource(R.drawable.tab_indicator_mytracks);
}
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackDataHub.start();
}
@Override
protected void onResume() {
super.onResume();
trackDataHub.loadTrack(trackId);
// Update UI
this.invalidateOptionsMenu();
trackController.onResume(isRecording(), recordingTrackPaused);
}
@Override
protected void onPause() {
super.onPause();
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
trackDataHub.stop();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
if (photoUri != null) {
outState.putParcelable(PHOTO_URI_KEY, photoUri);
}
outState.putBoolean(HAS_PHOTO_KEY, hasPhoto);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
return;
}
hasPhoto = resultCode == RESULT_OK;
if (hasPhoto) {
//Register photo in media scanner
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(photoUri);
this.sendBroadcast(mediaScanIntent);
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected int getLayoutResId() {
return R.layout.track_detail;
}
@Override
protected void onHomeSelected() {
/*
* TODO: Investigate
* According to
* http://developer.android.com/training/implementing-navigation
* /ancestral.html, we should use NavUtils.shouldUpRecreateTask instead of
* always creating a new back stack. However, NavUtils.shouldUpRecreateTask
* seems to always return false.
*/ */
TaskStackBuilder.create(this).addParentStack(TrackDetailActivity.class).startActivities(); public TrackDataHub getTrackDataHub() {
finish(); return trackDataHub;
} }
@Override /**
public void onNewIntent(Intent intent) { * Gets the track id.
setIntent(intent); */
handleIntent(intent); public long getTrackId() {
} return trackId;
}
@Override /**
public boolean onCreateOptionsMenu(Menu menu) { * Handles the data in the intent.
getMenuInflater().inflate(R.menu.track_detail, menu); */
private void handleIntent(Intent intent) {
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker); trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo); long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null); if (markerId != -1L) {
// Use the trackId from the marker
shareMenuItem = menu.findItem(R.id.track_detail_share); Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider(); if (waypoint == null) {
//TODO: Share the actual track when track is finished? How to get the file path or create a new file? finish();
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, trackId, "", TrackFileFormat.KML)); return;
}
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency); trackId = waypoint.getTrackId();
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(recordingTrackPaused);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_insert_marker:
intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
return false;
} }
takePicture(); if (trackId == -1L) {
return true; finish();
case R.id.track_detail_markers: return;
intent = IntentUtils.newIntent(this, MarkerListActivity.class) }
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId); Track track = contentProviderUtils.getTrack(trackId);
startActivity(intent); if (track == null) {
return true; // Use the last track if markerId is not set
case R.id.track_detail_voice_frequency: if (markerId == -1L) {
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency) track = contentProviderUtils.getLastTrack();
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG); if (track != null) {
return true; trackId = track.getId();
case R.id.track_detail_split_frequency: return;
FrequencyDialogFragment.newInstance(R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency) }
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG); }
return true; finish();
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_delete:
deleteTracks(new long[] { trackId });
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void takePicture() {
File dir = FileUtils.getPhotoDir(trackId);
FileUtils.ensureDirectoryExists(dir);
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
if (file != null) {
photoUri = FileProvider.getUriForFile(this, FileUtils.FILEPROVIDER, file);
Log.d(TAG, "Taking photo to URI: " + photoUri);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, CAMERA_REQUEST_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
takePicture();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
}
});
}
/**
* Gets the {@link TrackDataHub}.
*/
public TrackDataHub getTrackDataHub() {
return trackDataHub;
}
/**
* Gets the track id.
*/
public long getTrackId() {
return trackId;
}
/**
* Handles the data in the intent.
*/
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId != -1L) {
// Use the trackId from the marker
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
if (waypoint == null) {
finish();
return;
}
trackId = waypoint.getTrackId();
}
if (trackId == -1L) {
finish();
return;
}
Track track = contentProviderUtils.getTrack(trackId);
if (track == null) {
// Use the last track if markerId is not set
if (markerId == -1L) {
track = contentProviderUtils.getLastTrack();
if (track != null) {
trackId = track.getId();
return;
} }
}
finish();
} }
}
/** /**
* Updates the menu items. * Updates the menu items.
*/ */
private void updateMenuItems(boolean isPaused) { private void updateMenuItems(boolean isPaused) {
insertMarkerMenuItem.setVisible(isRecording() && !isPaused); insertMarkerMenuItem.setVisible(isRecording() && !isPaused);
insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused); insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused);
shareMenuItem.setVisible(!isRecording()); shareMenuItem.setVisible(!isRecording());
markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM); markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
voiceFrequencyMenuItem.setVisible(isRecording()); voiceFrequencyMenuItem.setVisible(isRecording());
splitFrequencyMenuItem.setVisible(isRecording()); splitFrequencyMenuItem.setVisible(isRecording());
String title; String title;
if (isRecording()) { if (isRecording()) {
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording); title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
} else { } else {
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
title = track != null ? track.getName() : ""; title = track != null ? track.getName() : "";
}
setTitle(title);
} }
setTitle(title);
}
public void chooseActivityType(String category) { public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG); ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
} }
@Override @Override
public void onChooseActivityTypeDone(String iconValue) { public void onChooseActivityTypeDone(String iconValue) {
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
String category = getString(TrackIconUtils.getIconActivityType(iconValue)); String category = getString(TrackIconUtils.getIconActivityType(iconValue));
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils); TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
} }
private boolean isRecording() { private boolean isRecording() {
return trackId == recordingTrackId; return trackId == recordingTrackId;
} }
} }
@@ -46,163 +46,163 @@ import com.google.android.maps.mytracks.R;
*/ */
public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeCaller { public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeCaller {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_NEW_TRACK = "new_track"; public static final String EXTRA_NEW_TRACK = "new_track";
private static final String TAG = TrackEditActivity.class.getSimpleName(); private static final String TAG = TrackEditActivity.class.getSimpleName();
private static final String ICON_VALUE_KEY = "icon_value_key"; private static final String ICON_VALUE_KEY = "icon_value_key";
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private Track track; private Track track;
private String iconValue; private String iconValue;
private EditText name; private EditText name;
private AutoCompleteTextView activityType; private AutoCompleteTextView activityType;
private Spinner activityTypeIcon; private Spinner activityTypeIcon;
private EditText description; private EditText description;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
Long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L); Long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) { if (trackId == -1L) {
Log.e(TAG, "invalid trackId"); Log.e(TAG, "invalid trackId");
finish(); finish();
return; return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(this);
track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId);
finish();
return;
}
name = findViewById(R.id.track_edit_name);
name.setText(track.getName());
activityType = findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position)));
}
});
activityType.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, activityType.getText().toString()));
} }
}
});
iconValue = null; contentProviderUtils = ContentProviderUtils.Factory.get(this);
if (bundle != null) { track = contentProviderUtils.getTrack(trackId);
iconValue = bundle.getString(ICON_VALUE_KEY); if (track == null) {
} Log.e(TAG, "No track for " + trackId);
if (iconValue == null) { finish();
iconValue = track.getIcon(); return;
}
name = findViewById(R.id.track_edit_name);
name.setText(track.getName());
activityType = findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position)));
}
});
activityType.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, activityType.getText().toString()));
}
}
});
iconValue = null;
if (bundle != null) {
iconValue = bundle.getString(ICON_VALUE_KEY);
}
if (iconValue == null) {
iconValue = track.getIcon();
}
activityTypeIcon = findViewById(R.id.track_edit_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
return true;
}
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
return true;
}
});
description = findViewById(R.id.track_edit_description);
description.setText(track.getDescription());
Button save = findViewById(R.id.track_edit_save);
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TrackUtils.updateTrack(TrackEditActivity.this, track, name.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
contentProviderUtils);
finish();
}
});
Button cancel = findViewById(R.id.track_edit_cancel);
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
setTitle(R.string.track_edit_new_track_title);
cancel.setVisibility(View.GONE);
} else {
setTitle(R.string.menu_edit);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
cancel.setVisibility(View.VISIBLE);
}
} }
activityTypeIcon = findViewById(R.id.track_edit_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
return true;
}
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
return true;
}
});
description = findViewById(R.id.track_edit_description); @Override
description.setText(track.getDescription()); protected void onStart() {
super.onStart();
Button save = findViewById(R.id.track_edit_save); TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TrackUtils.updateTrack(TrackEditActivity.this, track, name.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
contentProviderUtils);
finish();
}
});
Button cancel = findViewById(R.id.track_edit_cancel);
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
setTitle(R.string.track_edit_new_track_title);
cancel.setVisibility(View.GONE);
} else {
setTitle(R.string.menu_edit);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
cancel.setVisibility(View.VISIBLE);
} }
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
}
@Override @Override
protected void onStart() { public void onSaveInstanceState(Bundle outState) {
super.onStart(); super.onSaveInstanceState(outState);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); outState.putString(ICON_VALUE_KEY, iconValue);
} }
@Override @Override
protected void onStop() { protected int getLayoutResId() {
super.onStop(); return R.layout.track_edit;
trackRecordingServiceConnection.unbind(); }
}
@Override private void setActivityTypeIcon(String value) {
public void onSaveInstanceState(Bundle outState) { iconValue = value;
super.onSaveInstanceState(outState); TrackIconUtils.setIconSpinner(activityTypeIcon, value);
outState.putString(ICON_VALUE_KEY, iconValue); }
}
@Override @Override
protected int getLayoutResId() { public void onChooseActivityTypeDone(String value) {
return R.layout.track_edit; setActivityTypeIcon(value);
} activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
}
private void setActivityTypeIcon(String value) {
iconValue = value;
TrackIconUtils.setIconSpinner(activityTypeIcon, value);
}
@Override
public void onChooseActivityTypeDone(String value) {
setActivityTypeIcon(value);
activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
}
} }
File diff suppressed because it is too large Load Diff
@@ -16,14 +16,14 @@
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.location.LocationManager; import android.location.LocationManager;
import android.net.Uri; import android.net.Uri;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -34,368 +34,366 @@ import java.util.List;
*/ */
public interface ContentProviderUtils { public interface ContentProviderUtils {
/**
* The authority (the first part of the URI) for the app's content
* provider.
*/
String AUTHORITY = "com.google.android.maps.mytracks";
/**
* Clears a track. Removes waypoints and trackpoints. Only keeps the track id.
*
* @param trackId the track id
*/
void clearTrack(Context context, long trackId);
/**
* Creates a {@link Track} from a cursor.
*
* @param cursor the cursor pointing to the track
*/
Track createTrack(Cursor cursor);
/**
* Deletes all tracks (including waypoints and track points).
*/
void deleteAllTracks(Context context);
/**
* Deletes a track.
*
* @param trackId the track id
*/
void deleteTrack(Context context, long trackId);
/**
* Gets all the tracks. If no track exists, an empty list is returned.
* <p>
* Note that the returned tracks do not have any track points attached.
*/
List<Track> getAllTracks();
/**
* Gets the last track. Returns null if doesn't exist.
*/
Track getLastTrack();
/**
* Gets a track by a track id. Returns null if not found.
* <p>
* Note that the returned track doesn't have any track points attached.
*
* @param trackId the track id.
*/
Track getTrack(long trackId);
/**
* Gets a track cursor. The caller owns the returned cursor and is responsible
* for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null
*/
Cursor getTrackCursor(String selection, String[] selectionArgs, String sortOrder);
/**
* Inserts a track.
* <p>
* Note: This doesn't insert any track points.
*
* @param track the track
* @return the content provider URI of the inserted track.
*/
Uri insertTrack(Track track);
/**
* Updates a track.
* <p>
* Note: This doesn't update any track points.
*
* @param track the track
*/
void updateTrack(Track track);
/**
* Creates a waypoint from a cursor.
*
* @param cursor the cursor pointing to the waypoint
*/
Waypoint createWaypoint(Cursor cursor);
/**
* Deletes a waypoint. If deleting a statistics waypoint, this will also
* correct the next statistics waypoint after the deleted one to reflect the
* deletion. The generator is used to update the next statistics waypoint.
*
* @param waypointId the waypoint id
* @param descriptionGenerator the description generator. Can be null for
* waypoint marker
*/
void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator);
/**
* Gets the first waypoint id for a track. The first waypoint is special as it
* contains the stats for the track. Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getFirstWaypointId(long trackId);
/**
* Gets the last waypoint for a type. Returns null if it doesn't exist.
*
* @param trackId the track id
* @param waypointType the waypoint type
*/
Waypoint getLastWaypoint(long trackId, WaypointType waypointType);
/**
* Gets the next waypoint number for a type. Returns -1 if not able to get the
* next waypoint number.
*
* @param trackId the track id
* @param waypointType the waypoint type
*/
int getNextWaypointNumber(long trackId, WaypointType waypointType);
/**
* Gets a waypoint from a waypoint id. Returns null if not found.
*
* @param waypointId the waypoint id
*/
Waypoint getWaypoint(long waypointId);
/**
* Gets a waypoint cursor. The caller owns the returned cursor and is
* responsible for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null
* @param maxWaypoints the maximum number of waypoints to return. -1 for no
* limit
*/
Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints);
/**
* Gets a waypoint cursor for a track. The caller owns the returned cursor and
* is responsible for closing it.
*
* @param trackId the track id
* @param minWaypointId the minimum waypoint id. -1L to ignore
* @param maxWaypoints the maximum number of waypoints to return. -1 for no
* limit
*/
Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints);
/**
* Gets the number of waypoints for a track.
*
* @param trackId the track id
*/
int getWaypointCount(long trackId);
/**
* Inserts a waypoint.
*
* @param waypoint the waypoint
* @return the content provider URI of the inserted waypoint.
*/
Uri insertWaypoint(Waypoint waypoint);
/**
* Updates a waypoint. Returns true if successful.
*
* @param waypoint the waypoint
*/
boolean updateWaypoint(Waypoint waypoint);
/**
* Inserts multiple track points.
*
* @param locations an array of locations
* @param length the number of locations (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the track id
* @return the number of points inserted
*/
int bulkInsertTrackPoint(Location[] locations, int length, long trackId);
/**
* Creates a location object from a cursor.
*
* @param cursor the cursor pointing to the location
*/
Location createTrackPoint(Cursor cursor);
/**
* Gets the first location id for a track. Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getFirstTrackPointId(long trackId);
/**
* Gets the last location id for a track. Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getLastTrackPointId(long trackId);
/**
* Gets the track point id of a location.
*
* @param trackId the track id
* @param location the location
* @return track point id if the location is in the track. -1L otherwise.
*/
long getTrackPointId(long trackId, Location location);
/**
* Gets the first valid location for a track. Returns null if it doesn't
* exist.
*
* @param trackId the track id
*/
Location getFirstValidTrackPoint(long trackId);
/**
* Gets the last valid location for a track. Returns null if it doesn't exist.
*
* @param trackId the track id
*/
Location getLastValidTrackPoint(long trackId);
/**
* Gets the last valid location.
*/
Location getLastValidTrackPoint();
/**
* Creates a location cursor. The caller owns the returned cursor and is
* responsible for closing it.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest
* location first)
*/
Cursor getTrackPointCursor(
long trackId, long startTrackPointId, int maxLocations, boolean descending);
/**
* Creates a new read-only iterator over a given track's points. It provides a
* lightweight way of iterating over long tracks without failing due to the
* underlying cursor limitations. Since it's a read-only iterator,
* {@link Iterator#remove()} always throws
* {@link UnsupportedOperationException}. Each call to
* {@link LocationIterator#next()} may advance to the next DB record, and if
* so, the iterator calls {@link LocationFactory#createLocation()} and
* populates it with information retrieved from the record. When done with
* iteration, {@link LocationIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest
* location first)
* @param locationFactory the location factory
*/
LocationIterator getTrackPointLocationIterator(
long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
/**
* Inserts a track point.
*
* @param location the location
* @param trackId the track id
* @return the content provider URI of the inserted track point
*/
Uri insertTrackPoint(Location location, long trackId);
/**
* A lightweight wrapper around the original {@link Cursor} with a method to
* clean up.
*/
interface LocationIterator extends Iterator<Location> {
/** /**
* Gets the most recently retrieved track point id by {@link #next()}. * The authority (the first part of the URI) for the app's content
* provider.
*/ */
long getLocationId(); String AUTHORITY = "com.google.android.maps.mytracks";
/** /**
* Closes the iterator. * The default {@link LocationFactory} which creates a location each time.
*/ */
void close(); LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory() {
} @Override
public Location createLocation() {
/** return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
* A factory for creating new {@link Location}. }
*/ };
interface LocationFactory {
/** /**
* Creates a new {@link Location}. An implementation can create new * Clears a track. Removes waypoints and trackpoints. Only keeps the track id.
* instances or reuse existing instances for optimization.
*/
Location createLocation();
}
/**
* The default {@link LocationFactory} which creates a location each time.
*/
LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory() {
@Override
public Location createLocation() {
return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
}
};
/**
* A factory which can produce instances of {@link ContentProviderUtils}, and
* can be overridden for testing.
*/
class Factory {
private static Factory instance = new Factory();
/**
* Creates an instance of {@link ContentProviderUtils}.
* *
* @param context the context * @param trackId the track id
*/ */
public static ContentProviderUtils get(Context context) { void clearTrack(Context context, long trackId);
return instance.newForContext(context);
}
/** /**
* Returns the factory instance. * Creates a {@link Track} from a cursor.
*/
public static Factory getInstance() {
return instance;
}
/**
* Overrides the factory instance for testing. Don't forget to set it back
* to the original value after testing.
* *
* @param factory the factory * @param cursor the cursor pointing to the track
*/ */
public static void overrideInstance(Factory factory) { Track createTrack(Cursor cursor);
instance = factory;
/**
* Deletes all tracks (including waypoints and track points).
*/
void deleteAllTracks(Context context);
/**
* Deletes a track.
*
* @param trackId the track id
*/
void deleteTrack(Context context, long trackId);
/**
* Gets all the tracks. If no track exists, an empty list is returned.
* <p>
* Note that the returned tracks do not have any track points attached.
*/
List<Track> getAllTracks();
/**
* Gets the last track. Returns null if doesn't exist.
*/
Track getLastTrack();
/**
* Gets a track by a track id. Returns null if not found.
* <p>
* Note that the returned track doesn't have any track points attached.
*
* @param trackId the track id.
*/
Track getTrack(long trackId);
/**
* Gets a track cursor. The caller owns the returned cursor and is responsible
* for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null
*/
Cursor getTrackCursor(String selection, String[] selectionArgs, String sortOrder);
/**
* Inserts a track.
* <p>
* Note: This doesn't insert any track points.
*
* @param track the track
* @return the content provider URI of the inserted track.
*/
Uri insertTrack(Track track);
/**
* Updates a track.
* <p>
* Note: This doesn't update any track points.
*
* @param track the track
*/
void updateTrack(Track track);
/**
* Creates a waypoint from a cursor.
*
* @param cursor the cursor pointing to the waypoint
*/
Waypoint createWaypoint(Cursor cursor);
/**
* Deletes a waypoint. If deleting a statistics waypoint, this will also
* correct the next statistics waypoint after the deleted one to reflect the
* deletion. The generator is used to update the next statistics waypoint.
*
* @param waypointId the waypoint id
* @param descriptionGenerator the description generator. Can be null for
* waypoint marker
*/
void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator);
/**
* Gets the first waypoint id for a track. The first waypoint is special as it
* contains the stats for the track. Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getFirstWaypointId(long trackId);
/**
* Gets the last waypoint for a type. Returns null if it doesn't exist.
*
* @param trackId the track id
* @param waypointType the waypoint type
*/
Waypoint getLastWaypoint(long trackId, WaypointType waypointType);
/**
* Gets the next waypoint number for a type. Returns -1 if not able to get the
* next waypoint number.
*
* @param trackId the track id
* @param waypointType the waypoint type
*/
int getNextWaypointNumber(long trackId, WaypointType waypointType);
/**
* Gets a waypoint from a waypoint id. Returns null if not found.
*
* @param waypointId the waypoint id
*/
Waypoint getWaypoint(long waypointId);
/**
* Gets a waypoint cursor. The caller owns the returned cursor and is
* responsible for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null
* @param maxWaypoints the maximum number of waypoints to return. -1 for no
* limit
*/
Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints);
/**
* Gets a waypoint cursor for a track. The caller owns the returned cursor and
* is responsible for closing it.
*
* @param trackId the track id
* @param minWaypointId the minimum waypoint id. -1L to ignore
* @param maxWaypoints the maximum number of waypoints to return. -1 for no
* limit
*/
Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints);
/**
* Gets the number of waypoints for a track.
*
* @param trackId the track id
*/
int getWaypointCount(long trackId);
/**
* Inserts a waypoint.
*
* @param waypoint the waypoint
* @return the content provider URI of the inserted waypoint.
*/
Uri insertWaypoint(Waypoint waypoint);
/**
* Updates a waypoint. Returns true if successful.
*
* @param waypoint the waypoint
*/
boolean updateWaypoint(Waypoint waypoint);
/**
* Inserts multiple track points.
*
* @param locations an array of locations
* @param length the number of locations (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the track id
* @return the number of points inserted
*/
int bulkInsertTrackPoint(Location[] locations, int length, long trackId);
/**
* Creates a location object from a cursor.
*
* @param cursor the cursor pointing to the location
*/
Location createTrackPoint(Cursor cursor);
/**
* Gets the first location id for a track. Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getFirstTrackPointId(long trackId);
/**
* Gets the last location id for a track. Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getLastTrackPointId(long trackId);
/**
* Gets the track point id of a location.
*
* @param trackId the track id
* @param location the location
* @return track point id if the location is in the track. -1L otherwise.
*/
long getTrackPointId(long trackId, Location location);
/**
* Gets the first valid location for a track. Returns null if it doesn't
* exist.
*
* @param trackId the track id
*/
Location getFirstValidTrackPoint(long trackId);
/**
* Gets the last valid location for a track. Returns null if it doesn't exist.
*
* @param trackId the track id
*/
Location getLastValidTrackPoint(long trackId);
/**
* Gets the last valid location.
*/
Location getLastValidTrackPoint();
/**
* Creates a location cursor. The caller owns the returned cursor and is
* responsible for closing it.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest
* location first)
*/
Cursor getTrackPointCursor(
long trackId, long startTrackPointId, int maxLocations, boolean descending);
/**
* Creates a new read-only iterator over a given track's points. It provides a
* lightweight way of iterating over long tracks without failing due to the
* underlying cursor limitations. Since it's a read-only iterator,
* {@link Iterator#remove()} always throws
* {@link UnsupportedOperationException}. Each call to
* {@link LocationIterator#next()} may advance to the next DB record, and if
* so, the iterator calls {@link LocationFactory#createLocation()} and
* populates it with information retrieved from the record. When done with
* iteration, {@link LocationIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest
* location first)
* @param locationFactory the location factory
*/
LocationIterator getTrackPointLocationIterator(
long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
/**
* Inserts a track point.
*
* @param location the location
* @param trackId the track id
* @return the content provider URI of the inserted track point
*/
Uri insertTrackPoint(Location location, long trackId);
/**
* A lightweight wrapper around the original {@link Cursor} with a method to
* clean up.
*/
interface LocationIterator extends Iterator<Location> {
/**
* Gets the most recently retrieved track point id by {@link #next()}.
*/
long getLocationId();
/**
* Closes the iterator.
*/
void close();
} }
/** /**
* Creates an instance of {@link ContentProviderUtils}. Allows subclasses * A factory for creating new {@link Location}.
* to override for testing.
*
* @param context the context
*/ */
protected ContentProviderUtils newForContext(Context context) { interface LocationFactory {
return new ContentProviderUtilsImpl(context.getContentResolver());
/**
* Creates a new {@link Location}. An implementation can create new
* instances or reuse existing instances for optimization.
*/
Location createLocation();
}
/**
* A factory which can produce instances of {@link ContentProviderUtils}, and
* can be overridden for testing.
*/
class Factory {
private static Factory instance = new Factory();
/**
* Creates an instance of {@link ContentProviderUtils}.
*
* @param context the context
*/
public static ContentProviderUtils get(Context context) {
return instance.newForContext(context);
}
/**
* Returns the factory instance.
*/
public static Factory getInstance() {
return instance;
}
/**
* Overrides the factory instance for testing. Don't forget to set it back
* to the original value after testing.
*
* @param factory the factory
*/
public static void overrideInstance(Factory factory) {
instance = factory;
}
/**
* Creates an instance of {@link ContentProviderUtils}. Allows subclasses
* to override for testing.
*
* @param context the context
*/
protected ContentProviderUtils newForContext(Context context) {
return new ContentProviderUtilsImpl(context.getContentResolver());
}
} }
}
} }
File diff suppressed because it is too large Load Diff
@@ -23,13 +23,13 @@ package com.google.android.apps.mytracks.content;
*/ */
public class ContentTypeIds { public class ContentTypeIds {
private ContentTypeIds() {} public static final byte BOOLEAN_TYPE_ID = 0;
public static final byte LONG_TYPE_ID = 1;
public static final byte BOOLEAN_TYPE_ID = 0; public static final byte INT_TYPE_ID = 2;
public static final byte LONG_TYPE_ID = 1; public static final byte FLOAT_TYPE_ID = 3;
public static final byte INT_TYPE_ID = 2; public static final byte DOUBLE_TYPE_ID = 4;
public static final byte FLOAT_TYPE_ID = 3; public static final byte STRING_TYPE_ID = 5;
public static final byte DOUBLE_TYPE_ID = 4; public static final byte BLOB_TYPE_ID = 6;
public static final byte STRING_TYPE_ID = 5; private ContentTypeIds() {
public static final byte BLOB_TYPE_ID = 6; }
} }
@@ -49,370 +49,367 @@ import java.io.FileNotFoundException;
*/ */
public class CustomContentProvider extends ContentProvider { public class CustomContentProvider extends ContentProvider {
private static final String TAG = CustomContentProvider.class.getSimpleName();
@VisibleForTesting
static final int DATABASE_VERSION = 23;
@VisibleForTesting
static final String DATABASE_NAME = "database.db";
/**
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
this(context, DATABASE_NAME);
}
@VisibleForTesting @VisibleForTesting
public DatabaseHelper(Context context, String databaseName) { static final int DATABASE_VERSION = 23;
super(context, databaseName, null, DATABASE_VERSION); @VisibleForTesting
static final String DATABASE_NAME = "database.db";
private static final String TAG = CustomContentProvider.class.getSimpleName();
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
} }
@Override @Override
public void onCreate(SQLiteDatabase db) { public boolean onCreate() {
db.execSQL(TrackPointsColumns.CREATE_TABLE); return onCreate(getContext());
db.execSQL(TracksColumns.CREATE_TABLE); }
db.execSQL(WaypointsColumns.CREATE_TABLE);
/**
* Helper method to make onCreate is testable.
*
* @param context context to creates database
* @return true means run successfully
*/
@VisibleForTesting
boolean onCreate(Context context) {
DatabaseHelper databaseHelper = new DatabaseHelper(context);
try {
db = databaseHelper.getWritableDatabase();
} catch (SQLiteException e) {
Log.e(TAG, "Unable to open database for writing.", e);
}
return db != null;
} }
@Override @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
} String table;
} boolean shouldVacuum = false;
switch (getUrlType(url)) {
/** case TRACKPOINTS:
* Types of url. table = TrackPointsColumns.TABLE_NAME;
* break;
* @author Jimmy Shih case TRACKS:
*/ table = TracksColumns.TABLE_NAME;
@VisibleForTesting shouldVacuum = true;
enum UrlType { break;
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID case WAYPOINTS:
} table = WaypointsColumns.TABLE_NAME;
break;
private final UriMatcher uriMatcher; default:
private SQLiteDatabase db; throw new IllegalArgumentException("Unknown URL " + url);
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
}
@Override
public boolean onCreate() {
return onCreate(getContext());
}
/**
* Helper method to make onCreate is testable.
*
* @param context context to creates database
* @return true means run successfully
*/
@VisibleForTesting
boolean onCreate(Context context) {
DatabaseHelper databaseHelper = new DatabaseHelper(context);
try {
db = databaseHelper.getWritableDatabase();
} catch (SQLiteException e) {
Log.e(TAG, "Unable to open database for writing.", e);
}
return db != null;
}
@Override
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
String table;
boolean shouldVacuum = false;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
shouldVacuum = true;
break;
case WAYPOINTS:
table = WaypointsColumns.TABLE_NAME;
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(TAG, "Deleting table " + table);
int count;
try {
db.beginTransaction();
count = db.delete(table, where, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, reclaim its space.
Log.i(TAG, "Vacuuming the database.");
db.execSQL("VACUUM");
}
return count;
}
@Override
public String getType(@NonNull Uri url) {
switch (getUrlType(url)) {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_ID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_ID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
}
@Override
public Uri insert(@NonNull Uri url, ContentValues initialValues) {
if (initialValues == null) {
initialValues = new ContentValues();
}
Uri result;
try {
db.beginTransaction();
result = insertContentValues(url, getUrlType(url), initialValues);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return result;
}
@Override
public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] valuesBulk) {
int numInserted;
try {
// Use a transaction in order to make the insertions run as a single batch
db.beginTransaction();
UrlType urlType = getUrlType(url);
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
ContentValues contentValues = valuesBulk[numInserted];
if (contentValues == null) {
contentValues = new ContentValues();
} }
insertContentValues(url, urlType, contentValues);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return numInserted;
}
@Override Log.w(TAG, "Deleting table " + table);
public Cursor query(@NonNull Uri url, String[] projection, String selection, String[] selectionArgs, String sort) { int count;
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); try {
String sortOrder = null; db.beginTransaction();
switch (getUrlType(url)) { count = db.delete(table, where, selectionArgs);
case TRACKPOINTS: db.setTransactionSuccessful();
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); } finally {
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER; db.endTransaction();
break;
case TRACKPOINTS_ID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case WAYPOINTS:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
break;
case WAYPOINTS_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url);
return cursor;
}
@Override
public int update(@NonNull Uri url, ContentValues values, String where, String[] selectionArgs) {
String table;
String whereClause;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKPOINTS_ID:
table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
} }
break; getContext().getContentResolver().notifyChange(url, null, false);
case TRACKS:
table = TracksColumns.TABLE_NAME; if (shouldVacuum) {
whereClause = where; // If a potentially large amount of data was deleted, reclaim its space.
break; Log.i(TAG, "Vacuuming the database.");
case TRACKS_ID: db.execSQL("VACUUM");
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
} }
break; return count;
case WAYPOINTS: }
table = WaypointsColumns.TABLE_NAME;
whereClause = where; @Override
break; public String getType(@NonNull Uri url) {
case WAYPOINTS_ID: switch (getUrlType(url)) {
table = WaypointsColumns.TABLE_NAME; case TRACKPOINTS:
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1); return TrackPointsColumns.CONTENT_TYPE;
if (!TextUtils.isEmpty(where)) { case TRACKPOINTS_ID:
whereClause += " AND (" + where + ")"; return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_ID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
} }
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
} }
int count;
try {
db.beginTransaction();
count = db.update(table, values, whereClause, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return count;
}
@Override @Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { public Uri insert(@NonNull Uri url, ContentValues initialValues) {
try { if (initialValues == null) {
File file = new File(uri.getPath()); initialValues = new ContentValues();
if (file.exists()) { }
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); Uri result;
} try {
throw new FileNotFoundException(uri.getPath()); db.beginTransaction();
} finally { result = insertContentValues(url, getUrlType(url), initialValues);
File dir = new File(getContext().getCacheDir(), FileUtils.PLAY_TRACKS_DIR); db.setTransactionSuccessful();
for (File file : dir.listFiles()) { } finally {
file.delete(); db.endTransaction();
} }
getContext().revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); getContext().getContentResolver().notifyChange(url, null, false);
return result;
} }
}
/** @Override
* Gets the {@link UrlType} for a url. public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] valuesBulk) {
* int numInserted;
* @param url the url try {
*/ // Use a transaction in order to make the insertions run as a single batch
private UrlType getUrlType(Uri url) { db.beginTransaction();
return UrlType.values()[uriMatcher.match(url)];
}
/** UrlType urlType = getUrlType(url);
* Inserts a content based on the url type. for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
* ContentValues contentValues = valuesBulk[numInserted];
* @param url the content url if (contentValues == null) {
* @param urlType the url type contentValues = new ContentValues();
* @param contentValues the content values }
*/ insertContentValues(url, urlType, contentValues);
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) { }
switch (urlType) { db.setTransactionSuccessful();
case TRACKPOINTS: } finally {
return insertTrackPoint(url, contentValues); db.endTransaction();
case TRACKS: }
return insertTrack(url, contentValues); getContext().getContentResolver().notifyChange(url, null, false);
case WAYPOINTS: return numInserted;
return insertWaypoint(url, contentValues);
default:
throw new IllegalArgumentException("Unknown url " + url);
} }
}
/** @Override
* Inserts a track point. public Cursor query(@NonNull Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
* SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
* @param url the content url String sortOrder = null;
* @param values the content values switch (getUrlType(url)) {
*/ case TRACKPOINTS:
private Uri insertTrackPoint(Uri url, ContentValues values) { queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE); sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE); break;
boolean hasTime = values.containsKey(TrackPointsColumns.TIME); case TRACKPOINTS_ID:
if (!hasLatitude || !hasLongitude || !hasTime) { queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
throw new IllegalArgumentException("Latitude, longitude, and time values are required."); queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case WAYPOINTS:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
break;
case WAYPOINTS_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url);
return cursor;
} }
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLiteException("Failed to insert a track point " + url);
}
/** @Override
* Inserts a track. public int update(@NonNull Uri url, ContentValues values, String where, String[] selectionArgs) {
* String table;
* @param url the content url String whereClause;
* @param contentValues the content values switch (getUrlType(url)) {
*/ case TRACKPOINTS:
private Uri insertTrack(Uri url, ContentValues contentValues) { table = TrackPointsColumns.TABLE_NAME;
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME); whereClause = where;
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID); break;
if (!hasStartTime || !hasStartId) { case TRACKPOINTS_ID:
throw new IllegalArgumentException("Both start time and start id values are required."); table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKS_ID:
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case WAYPOINTS:
table = WaypointsColumns.TABLE_NAME;
whereClause = where;
break;
case WAYPOINTS_ID:
table = WaypointsColumns.TABLE_NAME;
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
int count;
try {
db.beginTransaction();
count = db.update(table, values, whereClause, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return count;
} }
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a track " + url);
}
/** @Override
* Inserts a waypoint. public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
* try {
* @param url the content url File file = new File(uri.getPath());
* @param contentValues the content values if (file.exists()) {
*/ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
private Uri insertWaypoint(Uri url, ContentValues contentValues) { }
long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, contentValues); throw new FileNotFoundException(uri.getPath());
if (rowId >= 0) { } finally {
return ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build(); File dir = new File(getContext().getCacheDir(), FileUtils.PLAY_TRACKS_DIR);
for (File file : dir.listFiles()) {
file.delete();
}
getContext().revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
/**
* Gets the {@link UrlType} for a url.
*
* @param url the url
*/
private UrlType getUrlType(Uri url) {
return UrlType.values()[uriMatcher.match(url)];
}
/**
* Inserts a content based on the url type.
*
* @param url the content url
* @param urlType the url type
* @param contentValues the content values
*/
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) {
switch (urlType) {
case TRACKPOINTS:
return insertTrackPoint(url, contentValues);
case TRACKS:
return insertTrack(url, contentValues);
case WAYPOINTS:
return insertWaypoint(url, contentValues);
default:
throw new IllegalArgumentException("Unknown url " + url);
}
}
/**
* Inserts a track point.
*
* @param url the content url
* @param values the content values
*/
private Uri insertTrackPoint(Uri url, ContentValues values) {
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
if (!hasLatitude || !hasLongitude || !hasTime) {
throw new IllegalArgumentException("Latitude, longitude, and time values are required.");
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLiteException("Failed to insert a track point " + url);
}
/**
* Inserts a track.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertTrack(Uri url, ContentValues contentValues) {
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID);
if (!hasStartTime || !hasStartId) {
throw new IllegalArgumentException("Both start time and start id values are required.");
}
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a track " + url);
}
/**
* Inserts a waypoint.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertWaypoint(Uri url, ContentValues contentValues) {
long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a waypoint " + url);
}
/**
* Types of url.
*
* @author Jimmy Shih
*/
@VisibleForTesting
enum UrlType {
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID
}
/**
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
this(context, DATABASE_NAME);
}
@VisibleForTesting
public DatabaseHelper(Context context, String databaseName) {
super(context, databaseName, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TrackPointsColumns.CREATE_TABLE);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
} }
throw new SQLException("Failed to insert a waypoint " + url);
}
} }
@@ -16,8 +16,6 @@
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.Constants;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@@ -25,6 +23,8 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.net.Uri; import android.net.Uri;
import com.google.android.apps.mytracks.Constants;
/** /**
* Data source on the phone. * Data source on the phone.
* *
@@ -32,49 +32,49 @@ import android.net.Uri;
*/ */
public class DataSource { public class DataSource {
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences; private final SharedPreferences sharedPreferences;
public DataSource(Context context) { public DataSource(Context context) {
contentResolver = context.getContentResolver(); contentResolver = context.getContentResolver();
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
} }
/** /**
* Registers a content observer. * Registers a content observer.
* *
* @param uri the uri * @param uri the uri
* @param observer the observer * @param observer the observer
*/ */
public void registerContentObserver(Uri uri, ContentObserver observer) { public void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer); contentResolver.registerContentObserver(uri, false, observer);
} }
/** /**
* Unregisters a content observer. * Unregisters a content observer.
* *
* @param observer the observer * @param observer the observer
*/ */
public void unregisterContentObserver(ContentObserver observer) { public void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer); contentResolver.unregisterContentObserver(observer);
} }
/** /**
* Registers a shared preference change listener. * Registers a shared preference change listener.
* *
* @param listener the listener * @param listener the listener
*/ */
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener); sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
} }
/** /**
* Unregisters a shared preference change listener. * Unregisters a shared preference change listener.
* *
* @param listener the listener * @param listener the listener
*/ */
public void unregisterOnSharedPreferenceChangeListener( public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) { OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
} }
} }
@@ -24,25 +24,25 @@ package com.google.android.apps.mytracks.content;
*/ */
public interface DataSourceListener { public interface DataSourceListener {
/** /**
* Notifies when the tracks table is updated. * Notifies when the tracks table is updated.
*/ */
void notifyTracksTableUpdated(); void notifyTracksTableUpdated();
/** /**
* Notifies when the waypoints table is updated. * Notifies when the waypoints table is updated.
*/ */
void notifyWaypointsTableUpdated(); void notifyWaypointsTableUpdated();
/** /**
* Notifies when the track points table is updated. * Notifies when the track points table is updated.
*/ */
void notifyTrackPointsTableUpdated(); void notifyTrackPointsTableUpdated();
/** /**
* Notifies when a preference changes. * Notifies when a preference changes.
* *
* @param key the preference key * @param key the preference key
*/ */
void notifyPreferenceChanged(String key); void notifyPreferenceChanged(String key);
} }
@@ -34,198 +34,194 @@ import java.util.Set;
*/ */
public class DataSourceManager { public class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName(); private static final String TAG = DataSourceManager.class.getSimpleName();
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
/** handler = new Handler();
* Observer when the tracks table is updated. tracksTableObserver = new TracksTableObserver();
* waypointsTableObserver = new WaypointsTableObserver();
* @author Jimmy Shih trackPointsTableObserver = new TrackPointsTableObserver();
*/ preferenceListener = new PreferenceListener();
private class TracksTableObserver extends ContentObserver {
public TracksTableObserver() {
super(handler);
} }
@Override /**
public void onChange(boolean selfChange) { * Updates listeners with data source.
dataSourceListener.notifyTracksTableUpdated(); *
} * @param listeners the listeners
}
/**
* Observer when the waypoints table is updated.
*
* @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyWaypointsTableUpdated();
}
}
/**
* Observer when the track points table is updated.
*
* @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTrackPointsTableUpdated();
}
}
/**
* Listener for preference changes.
*
* @author Jimmy Shih
*/
private class PreferenceListener implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
dataSourceListener.notifyPreferenceChanged(key);
}
}
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
handler = new Handler();
tracksTableObserver = new TracksTableObserver();
waypointsTableObserver = new WaypointsTableObserver();
trackPointsTableObserver = new TrackPointsTableObserver();
preferenceListener = new PreferenceListener();
}
/**
* Updates listeners with data source.
*
* @param listeners the listeners
*/
public void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/*
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
* the same internal listener
*/ */
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) { public void updateListeners(EnumSet<TrackDataType> listeners) {
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE); EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
/*
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
* the same internal listener
*/
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
}
Log.d(TAG, "Updating listeners " + neededListeners);
// Unnecessary = registered - needed
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners);
// Remove unnecessary listeners
for (TrackDataType trackDataType : unnecessaryListeners) {
unregisterListener(trackDataType);
}
// Add missing listeners
for (TrackDataType trackDataType : missingListeners) {
registerListener(trackDataType);
}
// Update registered listeners
registeredListeners.clear();
registeredListeners.addAll(neededListeners);
} }
Log.d(TAG, "Updating listeners " + neededListeners); /**
* Registers a listener with data source.
// Unnecessary = registered - needed *
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners); * @param trackDataType the listener data type
unnecessaryListeners.removeAll(neededListeners); */
private void registerListener(TrackDataType trackDataType) {
// Missing = needed - registered switch (trackDataType) {
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners); case TRACKS_TABLE:
missingListeners.removeAll(registeredListeners); dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
break;
// Remove unnecessary listeners case WAYPOINTS_TABLE:
for (TrackDataType trackDataType : unnecessaryListeners) { dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
unregisterListener(trackDataType); break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
} }
// Add missing listeners /**
for (TrackDataType trackDataType : missingListeners) { * Unregisters a listener with data source.
registerListener(trackDataType); *
* @param trackDataType listener data type
*/
private void unregisterListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.unregisterContentObserver(tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.unregisterContentObserver(waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.unregisterContentObserver(trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
} }
// Update registered listeners /**
registeredListeners.clear(); * Unregisters all listeners with data source.
registeredListeners.addAll(neededListeners); */
} public void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) {
/** unregisterListener(trackDataType);
* Registers a listener with data source. }
*
* @param trackDataType the listener data type
*/
private void registerListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
} }
}
/** /**
* Unregisters a listener with data source. * Observer when the tracks table is updated.
* *
* @param trackDataType listener data type * @author Jimmy Shih
*/ */
private void unregisterListener(TrackDataType trackDataType) { private class TracksTableObserver extends ContentObserver {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.unregisterContentObserver(tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.unregisterContentObserver(waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.unregisterContentObserver(trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
}
/** public TracksTableObserver() {
* Unregisters all listeners with data source. super(handler);
*/ }
public void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) { @Override
unregisterListener(trackDataType); public void onChange(boolean selfChange) {
dataSourceListener.notifyTracksTableUpdated();
}
}
/**
* Observer when the waypoints table is updated.
*
* @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyWaypointsTableUpdated();
}
}
/**
* Observer when the track points table is updated.
*
* @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTrackPointsTableUpdated();
}
}
/**
* Listener for preference changes.
*
* @author Jimmy Shih
*/
private class PreferenceListener implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
dataSourceListener.notifyPreferenceChanged(key);
}
} }
}
} }
@@ -27,21 +27,21 @@ import java.util.Vector;
*/ */
public interface DescriptionGenerator { public interface DescriptionGenerator {
/** /**
* Generates a track description. * Generates a track description.
* *
* @param track the track * @param track the track
* @param distances a vector of distances to generate the elevation chart * @param distances a vector of distances to generate the elevation chart
* @param elevations a vector of elevations to generate the elevation chart * @param elevations a vector of elevations to generate the elevation chart
* @param html true to output html, false to output plain text * @param html true to output html, false to output plain text
*/ */
String generateTrackDescription( String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html); Track track, Vector<Double> distances, Vector<Double> elevations, boolean html);
/** /**
* Generate a waypoint description from a trip statistics. * Generate a waypoint description from a trip statistics.
* *
* @param tripStatistics the trip statistics * @param tripStatistics the trip statistics
*/ */
String generateWaypointDescription(TripStatistics tripStatistics); String generateWaypointDescription(TripStatistics tripStatistics);
} }
@@ -17,6 +17,7 @@
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import android.content.Context; import android.content.Context;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
@@ -31,228 +32,228 @@ import java.util.Vector;
*/ */
public class DescriptionGeneratorImpl implements DescriptionGenerator { public class DescriptionGeneratorImpl implements DescriptionGenerator {
private static final String HTML_LINE_BREAK = "<br>"; private static final String HTML_LINE_BREAK = "<br>";
private static final String HTML_PARAGRAPH_SEPARATOR = "<p>"; private static final String HTML_PARAGRAPH_SEPARATOR = "<p>";
private static final String TEXT_LINE_BREAK = "\n"; private static final String TEXT_LINE_BREAK = "\n";
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n"; private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
private Context context; private Context context;
public DescriptionGeneratorImpl(Context context) { public DescriptionGeneratorImpl(Context context) {
this.context = context; this.context = context;
}
@Override
public String generateTrackDescription(Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String creator = html
? "<a href='http://" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
: context.getString(R.string.app_name);
builder.append(creator);
builder.append(paragraphSeparator);
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
return builder.toString();
}
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return generateTripStatisticsDescription(tripStatistics, false);
}
/**
* Writes a string to a string builder.
*
* @param text the string
* @param builder the string builder
* @param resId the resource id containing one string placeholder
* @param lineBreak the line break
*/
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
if (text == null || text.length() == 0) {
text = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, text));
builder.append(lineBreak);
}
/**
* Generates a description for a {@link TripStatistics}.
*
* @param stats the trip statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed,
lineBreak);
// Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace
writePace(stats.getAverageMovingSpeed(), builder,
R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(
double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
}
/**
* Writes pace.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
String[] paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
String[] paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
if (paceInMetrics[0] == null) {
paceInMetrics[0] = context.getString(R.string.value_unknown);
} }
if (paceInImperial[0] == null) { @Override
paceInImperial[0] = context.getString(R.string.value_unknown); public String generateTrackDescription(Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String creator = html
? "<a href='http://" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
: context.getString(R.string.app_name);
builder.append(creator);
builder.append(paragraphSeparator);
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
return builder.toString();
} }
builder.append(context.getString(resId, paceInMetrics[0], paceInImperial[0])); @Override
builder.append(lineBreak); public String generateWaypointDescription(TripStatistics tripStatistics) {
} return generateTripStatisticsDescription(tripStatistics, false);
}
/** /**
* Writes elevation. * Writes a string to a string builder.
* *
* @param elevation elevation in meters * @param text the string
* @param builder StringBuilder to append elevation * @param builder the string builder
* @param resId resource id of elevation string * @param resId the resource id containing one string placeholder
* @param lineBreak line break string * @param lineBreak the line break
*/ */
@VisibleForTesting private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
void writeElevation( if (text == null || text.length() == 0) {
double elevation, StringBuilder builder, int resId, String lineBreak) { text = context.getString(R.string.value_unknown);
long elevationInM = Math.round(elevation); }
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT); builder.append(context.getString(resId, text));
builder.append(context.getString(resId, elevationInM, elevationInFt)); builder.append(lineBreak);
builder.append(lineBreak); }
}
/** /**
* Writes grade. * Generates a description for a {@link TripStatistics}.
* *
* @param grade grade in fraction * @param stats the trip statistics
* @param builder StringBuilder to append grade * @param html true to use "<br>" for line break instead of "\n"
* @param resId resource id grade string */
* @param lineBreak line break string private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
*/ String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
@VisibleForTesting StringBuilder builder = new StringBuilder();
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L // Total distance
: Math.round(grade * 100); writeDistance(
builder.append(context.getString(resId, gradeInPercent)); stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
builder.append(lineBreak);
} // Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed,
lineBreak);
// Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace
writePace(stats.getAverageMovingSpeed(), builder,
R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(
double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
}
/**
* Writes pace.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
String[] paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
String[] paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
if (paceInMetrics[0] == null) {
paceInMetrics[0] = context.getString(R.string.value_unknown);
}
if (paceInImperial[0] == null) {
paceInImperial[0] = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, paceInMetrics[0], paceInImperial[0]));
builder.append(lineBreak);
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(
double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
: Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
} }
@@ -38,364 +38,383 @@ import java.util.TreeSet;
*/ */
public class SearchEngine { public class SearchEngine {
/** WHERE query to get tracks by name. */ /**
private static final String TRACK_SELECTION_QUERY = * WHERE query to get tracks by name.
TracksColumns.NAME + " LIKE ? OR " + */
TracksColumns.DESCRIPTION + " LIKE ? OR " + private static final String TRACK_SELECTION_QUERY =
TracksColumns.CATEGORY + " LIKE ?"; TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
/** WHERE query to get waypoints by name. */ /**
private static final String WAYPOINT_SELECTION_QUERY = * WHERE query to get waypoints by name.
WaypointsColumns.NAME + " LIKE ? OR " + */
WaypointsColumns.DESCRIPTION + " LIKE ? OR " + private static final String WAYPOINT_SELECTION_QUERY =
WaypointsColumns.CATEGORY + " LIKE ?"; WaypointsColumns.NAME + " LIKE ? OR " +
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
WaypointsColumns.CATEGORY + " LIKE ?";
/** Order of track results. */ /**
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000"; * Order of track results.
*/
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
/** Order of waypoint results. */ /**
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC"; * Order of waypoint results.
*/
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
/** How much we promote a match in the track category. */ /**
private static final double TRACK_CATEGORY_PROMOTION = 2.0; * How much we promote a match in the track category.
*/
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
/** How much we promote a match in the track description. */ /**
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0; * How much we promote a match in the track description.
*/
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
/** How much we promote a match in the track name. */ /**
private static final double TRACK_NAME_PROMOTION = 16.0; * How much we promote a match in the track name.
*/
private static final double TRACK_NAME_PROMOTION = 16.0;
/** How much we promote a waypoint result if it's in the currently-selected track. */ /**
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0; * How much we promote a waypoint result if it's in the currently-selected track.
*/
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
/** How much we promote a track result if it's the currently-selected track. */ /**
private static final double CURRENT_TRACK_DEMOTION = 0.5; * How much we promote a track result if it's the currently-selected track.
*/
private static final double CURRENT_TRACK_DEMOTION = 0.5;
/** Maximum number of waypoints which will be retrieved and scored. */ /**
private static final int MAX_SCORED_WAYPOINTS = 100; * Maximum number of waypoints which will be retrieved and scored.
*/
private static final int MAX_SCORED_WAYPOINTS = 100;
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */ /**
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L; * Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000)
*/
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Comparador for scored results.
*/
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
/** // Make tracks come before waypoints.
* Description of a search query, along with all contextual data needed to execute it. if (r1.waypoint != null && r2.track != null) {
*/ return 1;
public static class SearchQuery { } else if (r1.track != null && r2.waypoint != null) {
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, return -1;
long currentTimestamp) { }
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation; // Finally, use arbitrary ordering, by ID.
this.currentTrackId = currentTrackId; long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
this.currentTimestamp = currentTimestamp; long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final ContentProviderUtils providerUtils;
public SearchEngine(ContentProviderUtils providerUtils) {
this.providerUtils = providerUtils;
} }
public final String textQuery; /**
public final Location currentLocation; * Squashes a number by calculating 1 / log (1 + x).
public final long currentTrackId; */
public final long currentTimestamp; private static double squash(double x) {
} return 1.0 / Math.log1p(x);
/**
* Description of a search result which has been retrieved and scored.
*/
public static class ScoredResult {
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
} }
ScoredResult(Waypoint waypoint, double score) { /**
this.track = null; * Executes a search query and returns a set of sorted results.
this.waypoint = waypoint; *
this.score = score; * @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
ArrayList<Waypoint> waypoints = new ArrayList<>();
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults;
} }
public final Track track; /**
public final Waypoint waypoint; * Retrieves tracks matching the given query from the database.
public final double score; *
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[]{
queryLikeSelection,
queryLikeSelection,
queryLikeSelection};
@NonNull try (Cursor cursor = providerUtils.getTrackCursor(
@Override TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
public String toString() { if (cursor != null) {
return "ScoredResult [" tracks.ensureCapacity(cursor.getCount());
+ (track != null ? ("trackId=" + track.getId() + ", ") : "") while (cursor.moveToNext()) {
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "") tracks.add(providerUtils.createTrack(cursor));
+ "score=" + score + "]"; }
}
}
} }
}
/** Comparador for scored results. */ /**
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = * Retrieves waypoints matching the given query from the database.
new Comparator<ScoredResult>() { *
* @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[]{
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2};
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs,
WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
if (cursor != null) {
waypoints.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
Waypoint waypoint = providerUtils.createWaypoint(cursor);
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
waypoints.add(waypoint);
}
}
}
}
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId() == query.currentTrackId) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of waypoint results.
*
* @param waypoints the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
for (Waypoint waypoint : waypoints) {
// Calculate the score.
double score = scoreWaypointResult(query, waypoint);
// Add to the output.
output.add(new ScoredResult(waypoint, score));
}
}
/**
* Scores a single waypoint result.
*
* @param query the query to score for
* @param waypoint the results to score
* @return the score for the waypoint
*/
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
double score = 1.0;
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score waypoints in the currently-selected track higher (searching inside the current track).
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or waypoint
* @param description the description of the track or waypoint
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S
* UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Description of a search query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
public final String textQuery;
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
this.currentTimestamp = currentTimestamp;
}
}
/**
* Description of a search result which has been retrieved and scored.
*/
public static class ScoredResult {
public final Track track;
public final Waypoint waypoint;
public final double score;
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
this.score = score;
}
@NonNull
@Override @Override
public int compare(ScoredResult r1, ScoredResult r2) { public String toString() {
// Score ordering. return "ScoredResult ["
int scoreDiff = Double.compare(r2.score, r1.score); + (track != null ? ("trackId=" + track.getId() + ", ") : "")
if (scoreDiff != 0) { + (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
return scoreDiff; + "score=" + score + "]";
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
} }
};
private final ContentProviderUtils providerUtils;
public SearchEngine(ContentProviderUtils providerUtils) {
this.providerUtils = providerUtils;
}
/**
* Executes a search query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
ArrayList<Waypoint> waypoints = new ArrayList<>();
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults;
}
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[] {
queryLikeSelection,
queryLikeSelection,
queryLikeSelection };
try (Cursor cursor = providerUtils.getTrackCursor(
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
if (cursor != null) {
tracks.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
tracks.add(providerUtils.createTrack(cursor));
}
}
} }
}
/**
* Retrieves waypoints matching the given query from the database.
*
* @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[] {
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2 };
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs,
WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
if (cursor != null) {
waypoints.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
Waypoint waypoint = providerUtils.createWaypoint(cursor);
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
waypoints.add(waypoint);
}
}
}
}
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId() == query.currentTrackId) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of waypoint results.
*
* @param waypoints the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
for (Waypoint waypoint : waypoints) {
// Calculate the score.
double score = scoreWaypointResult(query, waypoint);
// Add to the output.
output.add(new ScoredResult(waypoint, score));
}
}
/**
* Scores a single waypoint result.
*
* @param query the query to score for
* @param waypoint the results to score
* @return the score for the waypoint
*/
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
double score = 1.0;
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score waypoints in the currently-selected track higher (searching inside the current track).
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or waypoint
* @param description the description of the track or waypoint
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S
* UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
} }
@@ -26,19 +26,19 @@ import android.provider.SearchRecentSuggestions;
*/ */
public class SearchEngineProvider extends SearchRecentSuggestionsProvider { public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = "com.google.android.maps.mytracks.search"; private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
private static final int MODE = DATABASE_MODE_QUERIES; private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() { public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE); setupSuggestions(AUTHORITY, MODE);
} }
// TODO: Also add suggestions from the database. // TODO: Also add suggestions from the database.
/** /**
* Creates and returns a helper for adding recent queries or clearing the recent query history. * Creates and returns a helper for adding recent queries or clearing the recent query history.
*/ */
public static SearchRecentSuggestions newHelper(Context context) { public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE); return new SearchRecentSuggestions(context, AUTHORITY, MODE);
} }
} }
@@ -26,41 +26,41 @@ import com.google.android.apps.mytracks.content.sensor.SensorDataSet;
*/ */
public class SensorDataSetLocation extends Location { public class SensorDataSetLocation extends Location {
private SensorDataSet sensorDataSet = null; private SensorDataSet sensorDataSet = null;
/** /**
* The id of this location from the provider. * The id of this location from the provider.
*/ */
private int id = -1; private int id = -1;
public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) { public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) {
super(location); super(location);
this.sensorDataSet = sensorDataSet; this.sensorDataSet = sensorDataSet;
} }
public SensorDataSetLocation(String provider) { public SensorDataSetLocation(String provider) {
super(provider); super(provider);
} }
public SensorDataSet getSensorDataSet() { public SensorDataSet getSensorDataSet() {
return sensorDataSet; return sensorDataSet;
} }
public void setSensorDataSet(SensorDataSet sensorDataSet) { public void setSensorDataSet(SensorDataSet sensorDataSet) {
this.sensorDataSet = sensorDataSet; this.sensorDataSet = sensorDataSet;
} }
public int getId() { public int getId() {
return id; return id;
} }
public void setId(int id) { public void setId(int id) {
this.id = id; this.id = id;
} }
public void reset() { public void reset() {
super.reset(); super.reset();
sensorDataSet = null; sensorDataSet = null;
id = -1; id = -1;
} }
} }
@@ -16,12 +16,12 @@
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.location.Location; import android.location.Location;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.google.android.apps.mytracks.stats.TripStatistics;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
@@ -32,172 +32,169 @@ import java.util.ArrayList;
*/ */
public class Track implements Parcelable { public class Track implements Parcelable {
private long id = -1L; public static final Parcelable.Creator<Track> CREATOR = new Parcelable.Creator<Track>() {
private String name = ""; @Override
private String description = ""; public Track createFromParcel(Parcel in) {
private String category = ""; return new Track(in);
private long startId = -1L; }
private long stopId = -1L;
/* @Override
* The number of location points (present even if the points themselves are public Track[] newArray(int size) {
* not loaded) return new Track[size];
*/ }
private int numberOfPoints = 0; };
private String icon = ""; private long id = -1L;
private long modifiedTime = -1L; private String name = "";
private String description = "";
private String category = "";
private long startId = -1L;
private long stopId = -1L;
/*
* The number of location points (present even if the points themselves are
* not loaded)
*/
private int numberOfPoints = 0;
private String icon = "";
private long modifiedTime = -1L;
private TripStatistics tripStatistics = new TripStatistics();
// Location points (which may not have been loaded)
private ArrayList<Location> locations = new ArrayList<>();
private TripStatistics tripStatistics = new TripStatistics(); public Track() {
// Location points (which may not have been loaded)
private ArrayList<Location> locations = new ArrayList<>();
public Track() {}
private Track(Parcel in) {
id = in.readLong();
name = in.readString();
description = in.readString();
category = in.readString();
startId = in.readLong();
stopId = in.readLong();
numberOfPoints = in.readInt();
icon = in.readString();
modifiedTime = in.readLong();
ClassLoader classLoader = getClass().getClassLoader();
tripStatistics = in.readParcelable(classLoader);
for (int i = 0; i < numberOfPoints; ++i) {
Location location = in.readParcelable(classLoader);
locations.add(location);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeInt(numberOfPoints);
dest.writeString(icon);
dest.writeLong(modifiedTime);
dest.writeParcelable(tripStatistics, 0);
for (int i = 0; i < numberOfPoints; ++i) {
dest.writeParcelable(locations.get(i), 0);
}
}
public static final Parcelable.Creator<Track> CREATOR = new Parcelable.Creator<Track>() {
@Override
public Track createFromParcel(Parcel in) {
return new Track(in);
} }
@Override private Track(Parcel in) {
public Track[] newArray(int size) { id = in.readLong();
return new Track[size]; name = in.readString();
description = in.readString();
category = in.readString();
startId = in.readLong();
stopId = in.readLong();
numberOfPoints = in.readInt();
icon = in.readString();
modifiedTime = in.readLong();
ClassLoader classLoader = getClass().getClassLoader();
tripStatistics = in.readParcelable(classLoader);
for (int i = 0; i < numberOfPoints; ++i) {
Location location = in.readParcelable(classLoader);
locations.add(location);
}
} }
};
public long getId() { @Override
return id; public int describeContents() {
} return 0;
}
public void setId(long id) { @Override
this.id = id; public void writeToParcel(Parcel dest, int flags) {
} dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeInt(numberOfPoints);
dest.writeString(icon);
dest.writeLong(modifiedTime);
public String getName() { dest.writeParcelable(tripStatistics, 0);
return name; for (int i = 0; i < numberOfPoints; ++i) {
} dest.writeParcelable(locations.get(i), 0);
}
}
public void setName(String name) { public long getId() {
this.name = name; return id;
} }
public String getDescription() { public void setId(long id) {
return description; this.id = id;
} }
public void setDescription(String description) { public String getName() {
this.description = description; return name;
} }
public String getCategory() { public void setName(String name) {
return category; this.name = name;
} }
public void setCategory(String category) { public String getDescription() {
this.category = category; return description;
} }
public long getStartId() { public void setDescription(String description) {
return startId; this.description = description;
} }
public void setStartId(long startId) { public String getCategory() {
this.startId = startId; return category;
} }
public long getStopId() { public void setCategory(String category) {
return stopId; this.category = category;
} }
public void setStopId(long stopId) { public long getStartId() {
this.stopId = stopId; return startId;
} }
public int getNumberOfPoints() { public void setStartId(long startId) {
return numberOfPoints; this.startId = startId;
} }
public void setNumberOfPoints(int numberOfPoints) { public long getStopId() {
this.numberOfPoints = numberOfPoints; return stopId;
} }
public String getIcon() { public void setStopId(long stopId) {
return icon; this.stopId = stopId;
} }
public void setIcon(String icon) { public int getNumberOfPoints() {
this.icon = icon; return numberOfPoints;
} }
public long getModifiedTime() { public void setNumberOfPoints(int numberOfPoints) {
return modifiedTime; this.numberOfPoints = numberOfPoints;
} }
public void setModifiedTime(long modifiedTime) { public String getIcon() {
this.modifiedTime = modifiedTime; return icon;
} }
public TripStatistics getTripStatistics() { public void setIcon(String icon) {
return tripStatistics; this.icon = icon;
} }
public void setTripStatistics(TripStatistics tripStatistics) { public long getModifiedTime() {
this.tripStatistics = tripStatistics; return modifiedTime;
} }
public void addLocation(Location location) { public void setModifiedTime(long modifiedTime) {
locations.add(location); this.modifiedTime = modifiedTime;
} }
public ArrayList<Location> getLocations() { public TripStatistics getTripStatistics() {
return locations; return tripStatistics;
} }
public void setLocations(ArrayList<Location> locations) { public void setTripStatistics(TripStatistics tripStatistics) {
this.locations = locations; this.tripStatistics = tripStatistics;
} }
public void addLocation(Location location) {
locations.add(location);
}
public ArrayList<Location> getLocations() {
return locations;
}
public void setLocations(ArrayList<Location> locations) {
this.locations = locations;
}
} }
File diff suppressed because it is too large Load Diff
@@ -25,93 +25,93 @@ import android.location.Location;
*/ */
public interface TrackDataListener { public interface TrackDataListener {
/** /**
* Called when the track or its statistics has been updated. * Called when the track or its statistics has been updated.
* *
* @param track the track * @param track the track
*/ */
void onTrackUpdated(Track track); void onTrackUpdated(Track track);
/** /**
* Called to clear previously-sent track points. * Called to clear previously-sent track points.
*/ */
void clearTrackPoints(); void clearTrackPoints();
/** /**
* Called when a sampled in track point is read. * Called when a sampled in track point is read.
* *
* @param location the location * @param location the location
*/ */
void onSampledInTrackPoint(Location location); void onSampledInTrackPoint(Location location);
/** /**
* Called when a sampled out track point is read. * Called when a sampled out track point is read.
* *
* @param location the location * @param location the location
*/ */
void onSampledOutTrackPoint(Location location); void onSampledOutTrackPoint(Location location);
/** /**
* Called when an invalid track point representing a segment split is read. * Called when an invalid track point representing a segment split is read.
*/ */
void onSegmentSplit(Location location); void onSegmentSplit(Location location);
/** /**
* Called when finish sending new track points. This gets called after every * Called when finish sending new track points. This gets called after every
* batch of calls to {@link #onSampledInTrackPoint(Location)}, * batch of calls to {@link #onSampledInTrackPoint(Location)},
* {@link #onSampledOutTrackPoint(Location)} and * {@link #onSampledOutTrackPoint(Location)} and
* {@link #onSegmentSplit(Location)}. * {@link #onSegmentSplit(Location)}.
*/ */
void onNewTrackPointsDone(); void onNewTrackPointsDone();
/** /**
* Called to clear previously sent waypoints. * Called to clear previously sent waypoints.
*/ */
void clearWaypoints(); void clearWaypoints();
/** /**
* Called when a new waypoint is read. * Called when a new waypoint is read.
* *
* @param waypoint the waypoint * @param waypoint the waypoint
*/ */
void onNewWaypoint(Waypoint waypoint); void onNewWaypoint(Waypoint waypoint);
/** /**
* Called when finish sending new waypoints. This gets called after every * Called when finish sending new waypoints. This gets called after every
* batch of calls to {@link #clearWaypoints()} and * batch of calls to {@link #clearWaypoints()} and
* {@link #onNewWaypoint(Waypoint)}. * {@link #onNewWaypoint(Waypoint)}.
*/ */
void onNewWaypointsDone(); void onNewWaypointsDone();
/** /**
* Called when the metric units preference value is change. * Called when the metric units preference value is change.
* *
* @param metricUnits true to use metric units, false to use imperial units * @param metricUnits true to use metric units, false to use imperial units
* @return true to reload all the data, false otherwise. * @return true to reload all the data, false otherwise.
*/ */
boolean onMetricUnitsChanged(boolean metricUnits); boolean onMetricUnitsChanged(boolean metricUnits);
/** /**
* Called when the report speed preference value is changed. * Called when the report speed preference value is changed.
* *
* @param reportSpeed true to report speed, false to report pace * @param reportSpeed true to report speed, false to report pace
* @return true to reload all the data, false otherwise. * @return true to reload all the data, false otherwise.
*/ */
boolean onReportSpeedChanged(boolean reportSpeed); boolean onReportSpeedChanged(boolean reportSpeed);
/** /**
* Called when the recording gps accuracy preference value is changed. * Called when the recording gps accuracy preference value is changed.
* *
* @param recordingGpsAccuracy the new value * @param recordingGpsAccuracy the new value
* @return true to reload all the data, false otherwise * @return true to reload all the data, false otherwise
*/ */
boolean onRecordingGpsAccuracy(int recordingGpsAccuracy); boolean onRecordingGpsAccuracy(int recordingGpsAccuracy);
/** /**
* Called when the recording distance interval preference value is changed. * Called when the recording distance interval preference value is changed.
* *
* @param recordingDistanceInterval the new value * @param recordingDistanceInterval the new value
* @return true to reload all the data, false otherwise. * @return true to reload all the data, false otherwise.
*/ */
boolean onRecordingDistanceIntervalChanged(int recordingDistanceInterval); boolean onRecordingDistanceIntervalChanged(int recordingDistanceInterval);
} }
@@ -33,91 +33,91 @@ import java.util.Set;
*/ */
public class TrackDataManager { public class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName(); private static final String TAG = TrackDataManager.class.getSimpleName();
// Map of listener to its track data types // Map of listener to its track data types
private final Map<TrackDataListener, EnumSet<TrackDataType>> private final Map<TrackDataListener, EnumSet<TrackDataType>>
listenerToTypesMap = new HashMap<>(); listenerToTypesMap = new HashMap<>();
// Map of track data type to listeners // Map of track data type to listeners
private final Map<TrackDataType, Set<TrackDataListener>> private final Map<TrackDataType, Set<TrackDataListener>>
typeToListenersMap = new EnumMap<>(TrackDataType.class); typeToListenersMap = new EnumMap<>(TrackDataType.class);
public TrackDataManager() { public TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) { for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>()); typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
} }
}
/**
* Registers a listener.
*
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
public void registerListener(
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return;
}
listenerToTypesMap.put(listener, trackDataTypes);
for (TrackDataType trackDataType : trackDataTypes) {
typeToListenersMap.get(trackDataType).add(listener);
}
}
/**
* Unregisters a listener.
*
* @param listener the listener
*/
public void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
return;
} }
// Remove the listener from the typeToListenersMap /**
for (TrackDataType trackDataType : removedTypes) { * Registers a listener.
typeToListenersMap.get(trackDataType).remove(listener); *
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
public void registerListener(
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return;
}
listenerToTypesMap.put(listener, trackDataTypes);
for (TrackDataType trackDataType : trackDataTypes) {
typeToListenersMap.get(trackDataType).add(listener);
}
} }
}
/** /**
* Gets the number of {@link TrackDataListener}. * Unregisters a listener.
*/ *
public int getNumberOfListeners() { * @param listener the listener
return listenerToTypesMap.size(); */
} public void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
return;
}
/** // Remove the listener from the typeToListenersMap
* Gets the track data types for a listener. for (TrackDataType trackDataType : removedTypes) {
* typeToListenersMap.get(trackDataType).remove(listener);
* @param listener the listener }
*/ }
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener); /**
} * Gets the number of {@link TrackDataListener}.
*/
/** public int getNumberOfListeners() {
* Gets the listeners for a {@link TrackDataType}. return listenerToTypesMap.size();
* }
* @param type the type
*/ /**
public Set<TrackDataListener> getListeners(TrackDataType type) { * Gets the track data types for a listener.
return typeToListenersMap.get(type); *
} * @param listener the listener
*/
/** public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
* Gets all the registered {@link TrackDataType}. return listenerToTypesMap.get(listener);
*/ }
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class); /**
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) { * Gets the listeners for a {@link TrackDataType}.
types.addAll(value); *
* @param type the type
*/
public Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
}
return types;
} }
return types;
}
} }
@@ -22,9 +22,9 @@ package com.google.android.apps.mytracks.content;
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public enum TrackDataType { public enum TrackDataType {
TRACKS_TABLE, // tracks table changes TRACKS_TABLE, // tracks table changes
WAYPOINTS_TABLE, // waypoints table changes WAYPOINTS_TABLE, // waypoints table changes
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
PREFERENCE // preference changes PREFERENCE // preference changes
} }
@@ -26,36 +26,36 @@ import android.provider.BaseColumns;
*/ */
public interface TrackPointsColumns extends BaseColumns { public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints"; String TABLE_NAME = "trackpoints";
Uri CONTENT_URI = Uri.parse("content://com.google.android.maps.mytracks/trackpoints"); Uri CONTENT_URI = Uri.parse("content://com.google.android.maps.mytracks/trackpoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.trackpoint"; String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.trackpoint"; String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.trackpoint";
String DEFAULT_SORT_ORDER = "_id"; String DEFAULT_SORT_ORDER = "_id";
// Columns // Columns
String TRACKID = "trackid"; String TRACKID = "trackid";
String LONGITUDE = "longitude"; String LONGITUDE = "longitude";
String LATITUDE = "latitude"; String LATITUDE = "latitude";
String TIME = "time"; String TIME = "time";
String ALTITUDE = "elevation"; String ALTITUDE = "elevation";
String ACCURACY = "accuracy"; String ACCURACY = "accuracy";
String SPEED = "speed"; String SPEED = "speed";
String BEARING = "bearing"; String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate"; String SENSOR_HEARTRATE = "sensor_heartrate";
String SENSOR_CADENCE = "sensor_cadence"; String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_POWER = "sensor_power"; String SENSOR_POWER = "sensor_power";
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TRACKID + " INTEGER, " + TRACKID + " INTEGER, "
+ LONGITUDE + " INTEGER, " + LONGITUDE + " INTEGER, "
+ LATITUDE + " INTEGER, " + LATITUDE + " INTEGER, "
+ TIME + " INTEGER, " + TIME + " INTEGER, "
+ ALTITUDE + " FLOAT, " + ALTITUDE + " FLOAT, "
+ ACCURACY + " FLOAT, " + ACCURACY + " FLOAT, "
+ SPEED + " FLOAT, " + SPEED + " FLOAT, "
+ BEARING + " FLOAT, " + BEARING + " FLOAT, "
+ SENSOR_HEARTRATE + " FLOAT, " + SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, " + SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT);"; + SENSOR_POWER + " FLOAT);";
} }
@@ -26,80 +26,80 @@ import android.provider.BaseColumns;
*/ */
public interface TracksColumns extends BaseColumns { public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks"; String TABLE_NAME = "tracks";
/** /**
* Tracks provider uri. * Tracks provider uri.
*/ */
Uri CONTENT_URI = Uri.parse("content://com.google.android.maps.mytracks/tracks"); Uri CONTENT_URI = Uri.parse("content://com.google.android.maps.mytracks/tracks");
/** /**
* Track content type. * Track content type.
*/ */
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.track"; String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.track";
/** /**
* Track id content type. * Track id content type.
*/ */
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.track"; String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.track";
/** /**
* Tracks table default sort order. * Tracks table default sort order.
*/ */
String DEFAULT_SORT_ORDER = "_id"; String DEFAULT_SORT_ORDER = "_id";
// Columns // Columns
String NAME = "name"; // track name String NAME = "name"; // track name
String DESCRIPTION = "description"; // track description String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type String CATEGORY = "category"; // track activity type
String STARTID = "startid"; // first track point id String STARTID = "startid"; // first track point id
String STOPID = "stopid"; // last track point id String STOPID = "stopid"; // last track point id
String STARTTIME = "starttime"; // track start time String STARTTIME = "starttime"; // track start time
String STOPTIME = "stoptime"; // track stop time String STOPTIME = "stoptime"; // track stop time
String NUMPOINTS = "numpoints"; // number of track points String NUMPOINTS = "numpoints"; // number of track points
String TOTALDISTANCE = "totaldistance"; // total distance String TOTALDISTANCE = "totaldistance"; // total distance
String TOTALTIME = "totaltime"; // total time String TOTALTIME = "totaltime"; // total time
String MOVINGTIME = "movingtime"; // moving time String MOVINGTIME = "movingtime"; // moving time
String MINLAT = "minlat"; // minimum latitude String MINLAT = "minlat"; // minimum latitude
String MAXLAT = "maxlat"; // maximum latitude String MAXLAT = "maxlat"; // maximum latitude
String MINLON = "minlon"; // minimum longitude String MINLON = "minlon"; // minimum longitude
String MAXLON = "maxlon"; // maximum longitude String MAXLON = "maxlon"; // maximum longitude
String AVGSPEED = "avgspeed"; // average speed String AVGSPEED = "avgspeed"; // average speed
// average moving speed // average moving speed
String AVGMOVINGSPEED = "avgmovingspeed"; String AVGMOVINGSPEED = "avgmovingspeed";
String MAXSPEED = "maxspeed"; // maximum speed String MAXSPEED = "maxspeed"; // maximum speed
String MINELEVATION = "minelevation"; // minimum elevation String MINELEVATION = "minelevation"; // minimum elevation
String MAXELEVATION = "maxelevation"; // maximum elevation String MAXELEVATION = "maxelevation"; // maximum elevation
String ELEVATIONGAIN = "elevationgain"; // elevation gain String ELEVATIONGAIN = "elevationgain"; // elevation gain
String MINGRADE = "mingrade"; // minimum grade String MINGRADE = "mingrade"; // minimum grade
String MAXGRADE = "maxgrade"; // maximum grade String MAXGRADE = "maxgrade"; // maximum grade
String ICON = "icon"; // track activity type icon String ICON = "icon"; // track activity type icon
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" // table String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" // table
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " // id + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " // id
+ NAME + " TEXT, " // name + NAME + " TEXT, " // name
+ DESCRIPTION + " TEXT, " // description + DESCRIPTION + " TEXT, " // description
+ CATEGORY + " TEXT, " // category + CATEGORY + " TEXT, " // category
+ STARTID + " INTEGER, " // start id + STARTID + " INTEGER, " // start id
+ STOPID + " INTEGER, " // stop id + STOPID + " INTEGER, " // stop id
+ STARTTIME + " INTEGER, " // start time + STARTTIME + " INTEGER, " // start time
+ STOPTIME + " INTEGER, " // stop time + STOPTIME + " INTEGER, " // stop time
+ NUMPOINTS + " INTEGER, " // num points + NUMPOINTS + " INTEGER, " // num points
+ TOTALDISTANCE + " FLOAT, " // total distance + TOTALDISTANCE + " FLOAT, " // total distance
+ TOTALTIME + " INTEGER, " // total time + TOTALTIME + " INTEGER, " // total time
+ MOVINGTIME + " INTEGER, " // moving time + MOVINGTIME + " INTEGER, " // moving time
+ MINLAT + " INTEGER, " // min latitude + MINLAT + " INTEGER, " // min latitude
+ MAXLAT + " INTEGER, " // max latitude + MAXLAT + " INTEGER, " // max latitude
+ MINLON + " INTEGER, " // min longitude + MINLON + " INTEGER, " // min longitude
+ MAXLON + " INTEGER, " // max longitude + MAXLON + " INTEGER, " // max longitude
+ AVGSPEED + " FLOAT, " // average speed + AVGSPEED + " FLOAT, " // average speed
+ AVGMOVINGSPEED + " FLOAT, " // average moving speed + AVGMOVINGSPEED + " FLOAT, " // average moving speed
+ MAXSPEED + " FLOAT, " // max speed + MAXSPEED + " FLOAT, " // max speed
+ MINELEVATION + " FLOAT, " // min elevation + MINELEVATION + " FLOAT, " // min elevation
+ MAXELEVATION + " FLOAT, " // max elevation + MAXELEVATION + " FLOAT, " // max elevation
+ ELEVATIONGAIN + " FLOAT, " // elevation gain + ELEVATIONGAIN + " FLOAT, " // elevation gain
+ MINGRADE + " FLOAT, " // min grade + MINGRADE + " FLOAT, " // min grade
+ MAXGRADE + " FLOAT, " // max grade + MAXGRADE + " FLOAT, " // max grade
+ ICON + " TEXT);"; // icon + ICON + " TEXT);"; // icon
} }
@@ -31,228 +31,228 @@ import com.google.android.apps.mytracks.stats.TripStatistics;
*/ */
public final class Waypoint implements Parcelable { public final class Waypoint implements Parcelable {
public enum WaypointType { public static final Parcelable.Creator<Waypoint> CREATOR = new Parcelable.Creator<Waypoint>() {
WAYPOINT, STATISTICS @Override
} public Waypoint createFromParcel(Parcel in) {
return new Waypoint(in);
}
private long id = -1L; @Override
private String name = ""; public Waypoint[] newArray(int size) {
private String description = ""; return new Waypoint[size];
private String category = ""; }
private String icon = ""; };
private long trackId = -1L; private long id = -1L;
private WaypointType type = WaypointType.WAYPOINT; private String name = "";
private double length = 0.0; private String description = "";
private long duration = 0; private String category = "";
private long startId = -1L; private String icon = "";
private long stopId = -1L; private long trackId = -1L;
private Location location = null; private WaypointType type = WaypointType.WAYPOINT;
private TripStatistics tripStatistics = null; private double length = 0.0;
private String photoUrl = ""; private long duration = 0;
private long startId = -1L;
private long stopId = -1L;
private Location location = null;
private TripStatistics tripStatistics = null;
private String photoUrl = "";
public Waypoint() {} public Waypoint() {
public Waypoint(String name, String description, String category, String icon, long trackId,
WaypointType type, double length, long duration, long startId, long stopId, Location location,
TripStatistics tripStatistics, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
this.icon = icon;
this.trackId = trackId;
this.type = type;
this.length = length;
this.duration = duration;
this.startId = startId;
this.stopId = stopId;
this.location = location;
this.tripStatistics = tripStatistics;
this.photoUrl = photoUrl;
}
private Waypoint(Parcel source) {
id = source.readLong();
name = source.readString();
description = source.readString();
category = source.readString();
icon = source.readString();
trackId = source.readLong();
type = WaypointType.values()[source.readInt()];
length = source.readDouble();
duration = source.readLong();
startId = source.readLong();
stopId = source.readLong();
ClassLoader classLoader = getClass().getClassLoader();
byte hasLocation = source.readByte();
if (hasLocation > 0) {
location = source.readParcelable(classLoader);
}
byte hasStats = source.readByte();
if (hasStats > 0) {
tripStatistics = source.readParcelable(classLoader);
}
photoUrl = source.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeString(icon);
dest.writeLong(trackId);
dest.writeInt(type.ordinal());
dest.writeDouble(length);
dest.writeLong(duration);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeByte(location == null ? (byte) 0 : (byte) 1);
if (location != null) {
dest.writeParcelable(location, 0);
}
dest.writeByte(tripStatistics == null ? (byte) 0 : (byte) 1);
if (tripStatistics != null) {
dest.writeParcelable(tripStatistics, 0);
}
dest.writeString(photoUrl);
}
public static final Parcelable.Creator<Waypoint> CREATOR = new Parcelable.Creator<Waypoint>() {
@Override
public Waypoint createFromParcel(Parcel in) {
return new Waypoint(in);
} }
@Override public Waypoint(String name, String description, String category, String icon, long trackId,
public Waypoint[] newArray(int size) { WaypointType type, double length, long duration, long startId, long stopId, Location location,
return new Waypoint[size]; TripStatistics tripStatistics, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
this.icon = icon;
this.trackId = trackId;
this.type = type;
this.length = length;
this.duration = duration;
this.startId = startId;
this.stopId = stopId;
this.location = location;
this.tripStatistics = tripStatistics;
this.photoUrl = photoUrl;
} }
};
public long getId() { private Waypoint(Parcel source) {
return id; id = source.readLong();
} name = source.readString();
description = source.readString();
category = source.readString();
icon = source.readString();
trackId = source.readLong();
type = WaypointType.values()[source.readInt()];
length = source.readDouble();
duration = source.readLong();
startId = source.readLong();
stopId = source.readLong();
public void setId(long id) { ClassLoader classLoader = getClass().getClassLoader();
this.id = id; byte hasLocation = source.readByte();
} if (hasLocation > 0) {
location = source.readParcelable(classLoader);
}
byte hasStats = source.readByte();
if (hasStats > 0) {
tripStatistics = source.readParcelable(classLoader);
}
photoUrl = source.readString();
}
public String getName() { @Override
return name; public int describeContents() {
} return 0;
}
public void setName(String name) { @Override
this.name = name; public void writeToParcel(Parcel dest, int flags) {
} dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeString(icon);
dest.writeLong(trackId);
dest.writeInt(type.ordinal());
dest.writeDouble(length);
dest.writeLong(duration);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeByte(location == null ? (byte) 0 : (byte) 1);
if (location != null) {
dest.writeParcelable(location, 0);
}
dest.writeByte(tripStatistics == null ? (byte) 0 : (byte) 1);
if (tripStatistics != null) {
dest.writeParcelable(tripStatistics, 0);
}
dest.writeString(photoUrl);
}
public String getDescription() { public long getId() {
return description; return id;
} }
public void setDescription(String description) { public void setId(long id) {
this.description = description; this.id = id;
} }
public String getCategory() { public String getName() {
return category; return name;
} }
public void setCategory(String category) { public void setName(String name) {
this.category = category; this.name = name;
} }
public String getIcon() { public String getDescription() {
return icon; return description;
} }
public void setIcon(String icon) { public void setDescription(String description) {
this.icon = icon; this.description = description;
} }
public long getTrackId() { public String getCategory() {
return trackId; return category;
} }
public void setTrackId(long trackId) { public void setCategory(String category) {
this.trackId = trackId; this.category = category;
} }
public WaypointType getType() { public String getIcon() {
return type; return icon;
} }
public void setType(WaypointType type) { public void setIcon(String icon) {
this.type = type; this.icon = icon;
} }
public double getLength() { public long getTrackId() {
return length; return trackId;
} }
public void setLength(double length) { public void setTrackId(long trackId) {
this.length = length; this.trackId = trackId;
} }
public long getDuration() { public WaypointType getType() {
return duration; return type;
} }
public void setDuration(long duration) { public void setType(WaypointType type) {
this.duration = duration; this.type = type;
} }
public long getStartId() { public double getLength() {
return startId; return length;
} }
public void setStartId(long startId) { public void setLength(double length) {
this.startId = startId; this.length = length;
} }
public long getStopId() { public long getDuration() {
return stopId; return duration;
} }
public void setStopId(long stopId) { public void setDuration(long duration) {
this.stopId = stopId; this.duration = duration;
} }
public Location getLocation() { public long getStartId() {
return location; return startId;
} }
public void setLocation(Location location) { public void setStartId(long startId) {
this.location = location; this.startId = startId;
} }
public TripStatistics getTripStatistics() { public long getStopId() {
return tripStatistics; return stopId;
} }
public void setTripStatistics(TripStatistics tripStatistics) { public void setStopId(long stopId) {
this.tripStatistics = tripStatistics; this.stopId = stopId;
} }
public String getPhotoUrl() { public Location getLocation() {
return photoUrl; return location;
} }
public Uri getPhotoURI() { public void setLocation(Location location) {
return Uri.parse(photoUrl); this.location = location;
} }
public boolean hasPhoto() { public TripStatistics getTripStatistics() {
return photoUrl != null && !"".equals(photoUrl); return tripStatistics;
} }
public void setPhotoUrl(String photoUrl) { public void setTripStatistics(TripStatistics tripStatistics) {
this.photoUrl = photoUrl; this.tripStatistics = tripStatistics;
} }
public String getPhotoUrl() {
return photoUrl;
}
public void setPhotoUrl(String photoUrl) {
this.photoUrl = photoUrl;
}
public Uri getPhotoURI() {
return Uri.parse(photoUrl);
}
public boolean hasPhoto() {
return photoUrl != null && !"".equals(photoUrl);
}
public enum WaypointType {
WAYPOINT, STATISTICS
}
} }
@@ -15,11 +15,11 @@
*/ */
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
/** /**
* A request for the service to create a waypoint at the current location. * A request for the service to create a waypoint at the current location.
* *
@@ -27,99 +27,97 @@ import android.os.Parcelable;
*/ */
public class WaypointCreationRequest implements Parcelable { public class WaypointCreationRequest implements Parcelable {
private WaypointType type; public final static WaypointCreationRequest DEFAULT_WAYPOINT = new WaypointCreationRequest(
// true if this marker contains the track statistics WaypointType.WAYPOINT, false);
private boolean isTrackStatistics; public final static WaypointCreationRequest DEFAULT_STATISTICS = new WaypointCreationRequest(
private String name; WaypointType.STATISTICS, false);
private String category; public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(
private String description; WaypointType.STATISTICS, true);
private String iconUrl; public static final Creator CREATOR = new Creator();
private String photoUrl; private WaypointType type;
// true if this marker contains the track statistics
private boolean isTrackStatistics;
private String name;
private String category;
private String description;
private String iconUrl;
private String photoUrl;
public final static WaypointCreationRequest DEFAULT_WAYPOINT = new WaypointCreationRequest( private WaypointCreationRequest(WaypointType type, boolean isTrackStatistics) {
WaypointType.WAYPOINT, false); this(type, isTrackStatistics, null, null, null, null, null);
public final static WaypointCreationRequest DEFAULT_STATISTICS = new WaypointCreationRequest( }
WaypointType.STATISTICS, false);
public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(
WaypointType.STATISTICS, true);
private WaypointCreationRequest(WaypointType type, boolean isTrackStatistics) { public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name,
this(type, isTrackStatistics, null, null, null, null, null); String category, String description, String iconUrl, String photoUrl) {
} this.type = type;
this.isTrackStatistics = isTrackStatistics;
public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name, this.name = name;
String category, String description, String iconUrl, String photoUrl) { this.category = category;
this.type = type; this.description = description;
this.isTrackStatistics = isTrackStatistics; this.iconUrl = iconUrl;
this.name = name; this.photoUrl = photoUrl;
this.category = category; }
this.description = description;
this.iconUrl = iconUrl;
this.photoUrl = photoUrl;
}
public static class Creator implements Parcelable.Creator<WaypointCreationRequest> {
@Override @Override
public WaypointCreationRequest createFromParcel(Parcel source) { public int describeContents() {
return new WaypointCreationRequest( return 0;
WaypointType.values()[source.readInt()],
source.readByte() == 1,
source.readString(),
source.readString(),
source.readString(),
source.readString(),
source.readString());
} }
public WaypointCreationRequest[] newArray(int size) { @Override
return new WaypointCreationRequest[size]; public void writeToParcel(Parcel parcel, int arg1) {
parcel.writeInt(type.ordinal());
parcel.writeByte((byte) (isTrackStatistics ? 1 : 0));
parcel.writeString(name);
parcel.writeString(category);
parcel.writeString(description);
parcel.writeString(iconUrl);
parcel.writeString(photoUrl);
} }
}
public static final Creator CREATOR = new Creator(); public WaypointType getType() {
return type;
}
@Override public boolean isTrackStatistics() {
public int describeContents() { return isTrackStatistics;
return 0; }
}
@Override public String getName() {
public void writeToParcel(Parcel parcel, int arg1) { return name;
parcel.writeInt(type.ordinal()); }
parcel.writeByte((byte) (isTrackStatistics ? 1 : 0));
parcel.writeString(name);
parcel.writeString(category);
parcel.writeString(description);
parcel.writeString(iconUrl);
parcel.writeString(photoUrl);
}
public WaypointType getType() { public String getCategory() {
return type; return category;
} }
public boolean isTrackStatistics() { public String getDescription() {
return isTrackStatistics; return description;
} }
public String getName() { public String getIconUrl() {
return name; return iconUrl;
} }
public String getCategory() { public String getPhotoUrl() {
return category; return photoUrl;
} }
public String getDescription() { public static class Creator implements Parcelable.Creator<WaypointCreationRequest> {
return description;
}
public String getIconUrl() { @Override
return iconUrl; public WaypointCreationRequest createFromParcel(Parcel source) {
} return new WaypointCreationRequest(
WaypointType.values()[source.readInt()],
source.readByte() == 1,
source.readString(),
source.readString(),
source.readString(),
source.readString(),
source.readString());
}
public String getPhotoUrl() { public WaypointCreationRequest[] newArray(int size) {
return photoUrl; return new WaypointCreationRequest[size];
} }
}
} }
@@ -26,78 +26,78 @@ import android.provider.BaseColumns;
*/ */
public interface WaypointsColumns extends BaseColumns { public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints"; String TABLE_NAME = "waypoints";
Uri CONTENT_URI = Uri.parse("content://com.google.android.maps.mytracks/waypoints"); Uri CONTENT_URI = Uri.parse("content://com.google.android.maps.mytracks/waypoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.waypoint"; String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.waypoint"; String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.google.waypoint";
String DEFAULT_SORT_ORDER = "_id"; String DEFAULT_SORT_ORDER = "_id";
// Columns // Columns
String NAME = "name"; // waypoint name String NAME = "name"; // waypoint name
String DESCRIPTION = "description"; // waypoint description String DESCRIPTION = "description"; // waypoint description
String CATEGORY = "category"; // waypoint category String CATEGORY = "category"; // waypoint category
String ICON = "icon"; // waypoint icon String ICON = "icon"; // waypoint icon
String TRACKID = "trackid"; // track id String TRACKID = "trackid"; // track id
String TYPE = "type"; // type String TYPE = "type"; // type
String LENGTH = "length"; // length of the track (without smoothing) String LENGTH = "length"; // length of the track (without smoothing)
String DURATION = "duration"; // total duration of the track (not from last waypoint) String DURATION = "duration"; // total duration of the track (not from last waypoint)
String STARTTIME = "starttime"; // start time of the trip statistics String STARTTIME = "starttime"; // start time of the trip statistics
String STARTID = "startid"; // start track point id String STARTID = "startid"; // start track point id
String STOPID = "stopid"; // stop track point id String STOPID = "stopid"; // stop track point id
String LONGITUDE = "longitude"; // longitude String LONGITUDE = "longitude"; // longitude
String LATITUDE = "latitude"; // latitude String LATITUDE = "latitude"; // latitude
String TIME = "time"; // time String TIME = "time"; // time
String ALTITUDE = "elevation"; // altitude String ALTITUDE = "elevation"; // altitude
String ACCURACY = "accuracy"; // accuracy String ACCURACY = "accuracy"; // accuracy
String SPEED = "speed"; // speed String SPEED = "speed"; // speed
String BEARING = "bearing"; // bearing String BEARING = "bearing"; // bearing
String TOTALDISTANCE = "totaldistance"; // total distance String TOTALDISTANCE = "totaldistance"; // total distance
String TOTALTIME = "totaltime"; // total time String TOTALTIME = "totaltime"; // total time
String MOVINGTIME = "movingtime"; // moving time String MOVINGTIME = "movingtime"; // moving time
String AVGSPEED = "avgspeed"; // average speed String AVGSPEED = "avgspeed"; // average speed
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
String MAXSPEED = "maxspeed"; // max speed String MAXSPEED = "maxspeed"; // max speed
String MINELEVATION = "minelevation"; // min elevation String MINELEVATION = "minelevation"; // min elevation
String MAXELEVATION = "maxelevation"; // max elevation String MAXELEVATION = "maxelevation"; // max elevation
String ELEVATIONGAIN = "elevationgain"; // elevation gain String ELEVATIONGAIN = "elevationgain"; // elevation gain
String MINGRADE = "mingrade"; // min grade String MINGRADE = "mingrade"; // min grade
String MAXGRADE = "maxgrade"; // max grade String MAXGRADE = "maxgrade"; // max grade
String PHOTOURL = "photoUrl"; // url for the photo String PHOTOURL = "photoUrl"; // url for the photo
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ NAME + " TEXT, " + NAME + " TEXT, "
+ DESCRIPTION + " TEXT, " + DESCRIPTION + " TEXT, "
+ CATEGORY + " TEXT, " + CATEGORY + " TEXT, "
+ ICON + " TEXT, " + ICON + " TEXT, "
+ TRACKID + " INTEGER, " + TRACKID + " INTEGER, "
+ TYPE + " INTEGER, " + TYPE + " INTEGER, "
+ LENGTH + " FLOAT, " + LENGTH + " FLOAT, "
+ DURATION + " INTEGER, " + DURATION + " INTEGER, "
+ STARTTIME + " INTEGER, " + STARTTIME + " INTEGER, "
+ STARTID + " INTEGER, " + STARTID + " INTEGER, "
+ STOPID + " INTEGER, " + STOPID + " INTEGER, "
+ LONGITUDE + " INTEGER, " + LONGITUDE + " INTEGER, "
+ LATITUDE + " INTEGER, " + LATITUDE + " INTEGER, "
+ TIME + " INTEGER, " + TIME + " INTEGER, "
+ ALTITUDE + " FLOAT, " + ALTITUDE + " FLOAT, "
+ ACCURACY + " FLOAT, " + ACCURACY + " FLOAT, "
+ SPEED + " FLOAT, " + SPEED + " FLOAT, "
+ BEARING + " FLOAT, " + BEARING + " FLOAT, "
+ TOTALDISTANCE + " FLOAT, " + TOTALDISTANCE + " FLOAT, "
+ TOTALTIME + " INTEGER, " + TOTALTIME + " INTEGER, "
+ MOVINGTIME + " INTEGER, " + MOVINGTIME + " INTEGER, "
+ AVGSPEED + " FLOAT, " + AVGSPEED + " FLOAT, "
+ AVGMOVINGSPEED + " FLOAT, " + AVGMOVINGSPEED + " FLOAT, "
+ MAXSPEED + " FLOAT, " + MAXSPEED + " FLOAT, "
+ MINELEVATION + " FLOAT, " + MINELEVATION + " FLOAT, "
+ MAXELEVATION + " FLOAT, " + MAXELEVATION + " FLOAT, "
+ ELEVATIONGAIN + " FLOAT, " + ELEVATIONGAIN + " FLOAT, "
+ MINGRADE + " FLOAT, " + MINGRADE + " FLOAT, "
+ MAXGRADE + " FLOAT, " + MAXGRADE + " FLOAT, "
+ PHOTOURL + " TEXT" + PHOTOURL + " TEXT"
+ ");"; + ");";
} }
@@ -5,16 +5,14 @@ public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Integer.MAX_VALUE; public static final float DATA_UNAVAILABLE = Integer.MAX_VALUE;
//TODO It might be necessary to consider: sensor if sensor was connected as well. //TODO It might be necessary to consider: sensor if sensor was connected as well.
public String sensorName;
public String sensorAddress;
private float heartRate; private float heartRate;
private float cadence; private float cadence;
private float power; private float power;
private float batteryLevel; private float batteryLevel;
private long creationTimestamp; private long creationTimestamp;
public String sensorName;
public String sensorAddress;
public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long creationTimestamp) { public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long creationTimestamp) {
this.heartRate = heartRate; this.heartRate = heartRate;
this.cadence = cadence; this.cadence = cadence;
@@ -71,6 +69,7 @@ public final class SensorDataSet {
/** /**
* Is the data recent considering the current time. * Is the data recent considering the current time.
*
* @param maxAge the maximal age in milliseconds. * @param maxAge the maximal age in milliseconds.
*/ */
public boolean isRecent(long maxAge) { public boolean isRecent(long maxAge) {
@@ -16,33 +16,33 @@
package com.google.android.apps.mytracks.fragments; package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.DialogUtils;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
/** import com.google.android.apps.mytracks.util.DialogUtils;
/**
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public abstract class AbstractDialogFragment extends DialogFragment { public abstract class AbstractDialogFragment extends DialogFragment {
@NonNull @NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = createDialog(); final Dialog dialog = createDialog();
dialog.setOnShowListener(new DialogInterface.OnShowListener() { dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override @Override
public void onShow(DialogInterface dialogInterface) { public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(getActivity(), dialog); DialogUtils.setDialogTitleDivider(getActivity(), dialog);
} }
}); });
return dialog; return dialog;
} }
protected abstract Dialog createDialog(); protected abstract Dialog createDialog();
} }
@@ -56,466 +56,466 @@ import java.util.EnumSet;
*/ */
public class ChartFragment extends Fragment implements TrackDataListener { public class ChartFragment extends Fragment implements TrackDataListener {
public static final String CHART_FRAGMENT_TAG = "chartFragment"; public static final String CHART_FRAGMENT_TAG = "chartFragment";
private final ArrayList<double[]> pendingPoints = new ArrayList<>(); private final ArrayList<double[]> pendingPoints = new ArrayList<>();
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
// Stats gathered from the received data // Stats gathered from the received data
private TripStatisticsUpdater tripStatisticsUpdater; private TripStatisticsUpdater tripStatisticsUpdater;
private long startTime; private long startTime;
private boolean metricUnits = true; private boolean metricUnits = true;
private boolean reportSpeed = true; private boolean reportSpeed = true;
private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT; private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT;
// Modes of operation // Modes of operation
private boolean chartByDistance = true; private boolean chartByDistance = true;
private boolean[] chartShow = new boolean[] { true, true, true, true, true, true }; private boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
// UI elements // UI elements
private ChartView chartView; private ChartView chartView;
private ZoomControls zoomControls; private ZoomControls zoomControls;
/** /**
* A runnable that will enable/disable zoom controls and orange pointer as * A runnable that will enable/disable zoom controls and orange pointer as
* appropriate and redraw. * appropriate and redraw.
*/
private final Runnable updateChart = new Runnable() {
@Override
public void run() {
if (!isResumed() || trackDataHub == null) {
return;
}
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* Create a chartView here to store data thus won't need to reload all the
* data on every onStart or onResume.
*/ */
chartView = new ChartView(getActivity()); private final Runnable updateChart = new Runnable() {
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
}
@Override
public void onStart() {
super.onStart();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams);
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
checkChartSettings();
getActivity().runOnUiThread(updateChart);
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
}
@Override
public void onStop() {
super.onStop();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
layout.removeView(chartView);
}
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
if (track == null || track.getTripStatistics() == null) {
startTime = -1L;
return;
}
startTime = track.getTripStatistics().getStartTime();
}
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
pendingPoints.clear();
chartView.reset();
runOnUiThread(new Runnable() {
@Override
public void run() { public void run() {
if (isResumed()) { if (!isResumed() || trackDataHub == null) {
chartView.resetScroll(); return;
} }
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
} }
}); };
}
}
@Override @Override
public void onSampledInTrackPoint(Location location) { public void onCreate(Bundle savedInstanceState) {
if (isResumed()) { super.onCreate(savedInstanceState);
double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data);
pendingPoints.add(data);
}
}
@Override /*
public void onSampledOutTrackPoint(Location location) { * Create a chartView here to store data thus won't need to reload all the
if (isResumed()) { * data on every onStart or onResume.
fillDataPoint(location, null); */
chartView = new ChartView(getActivity());
} }
}
@Override @Override
public void onSegmentSplit(Location location) { public View onCreateView(
if (isResumed()) { @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
fillDataPoint(location, null); View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
} }
}
@Override @Override
public void onNewTrackPointsDone() { public void onStart() {
if (isResumed()) { super.onStart();
chartView.addDataPoints(pendingPoints); ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
pendingPoints.clear(); LayoutParams layoutParams = new LayoutParams(
runOnUiThread(updateChart); LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams);
} }
}
@Override @Override
public void clearWaypoints() { public void onResume() {
if (isResumed()) { super.onResume();
chartView.clearWaypoints(); resumeTrackDataHub();
checkChartSettings();
getActivity().runOnUiThread(updateChart);
} }
}
@Override @Override
public void onNewWaypoint(Waypoint waypoint) { public void onPause() {
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) { super.onPause();
chartView.addWaypoint(waypoint); pauseTrackDataHub();
} }
}
@Override @Override
public void onNewWaypointsDone() { public void onStop() {
if (isResumed()) { super.onStop();
runOnUiThread(updateChart); ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
layout.removeView(chartView);
} }
}
@Override @Override
public boolean onMetricUnitsChanged(boolean metric) { public void onTrackUpdated(Track track) {
if (isResumed()) { if (isResumed()) {
if (metricUnits == metric) { if (track == null || track.getTripStatistics() == null) {
startTime = -1L;
return;
}
startTime = track.getTripStatistics().getStartTime();
}
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
pendingPoints.clear();
chartView.reset();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.resetScroll();
}
}
});
}
}
@Override
public void onSampledInTrackPoint(Location location) {
if (isResumed()) {
double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data);
pendingPoints.add(data);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
chartView.addDataPoints(pendingPoints);
pendingPoints.clear();
runOnUiThread(updateChart);
}
}
@Override
public void clearWaypoints() {
if (isResumed()) {
chartView.clearWaypoints();
}
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
chartView.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
if (isResumed()) {
runOnUiThread(updateChart);
}
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
if (isResumed()) {
if (metricUnits == metric) {
return false;
}
metricUnits = metric;
chartView.setMetricUnits(metricUnits);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
}
return false; return false;
}
metricUnits = metric;
chartView.setMetricUnits(metricUnits);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
} }
return false;
}
@Override @Override
public boolean onReportSpeedChanged(boolean speed) { public boolean onReportSpeedChanged(boolean speed) {
if (isResumed()) { if (isResumed()) {
if (reportSpeed == speed) { if (reportSpeed == speed) {
return false;
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
}
return false; return false;
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
} }
return false;
}
@Override @Override
public boolean onRecordingGpsAccuracy(int minRequiredAccuracy) { public boolean onRecordingGpsAccuracy(int minRequiredAccuracy) {
// We don't care. // We don't care.
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int value) {
if (isResumed()) {
if (recordingDistanceInterval == value) {
return false; return false;
}
recordingDistanceInterval = value;
return true;
}
return false;
}
/**
* Checks the chart settings.
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) {
chartByDistance = !chartByDistance;
chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
needUpdate = true;
} }
boolean chartShowSpeed = PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT); @Override
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) { public boolean onRecordingDistanceIntervalChanged(int value) {
needUpdate = true; if (isResumed()) {
} if (recordingDistanceInterval == value) {
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) { return false;
needUpdate = true; }
} recordingDistanceInterval = value;
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_power_key, PreferencesUtils.CHART_SHOW_POWER_DEFAULT))) { return true;
needUpdate = true;
}
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_cadence_key, PreferencesUtils.CHART_SHOW_CADENCE_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_heart_rate_key, PreferencesUtils.CHART_SHOW_HEART_RATE_DEFAULT))) {
needUpdate = true;
}
if (needUpdate) {
chartView.postInvalidate();
}
}
/**
* Sets the series enabled value.
*
* @param index the series index
* @param value the value
* @return true if changed
*/
private boolean setSeriesEnabled(int index, boolean value) {
if (chartShow[index] != value) {
chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value);
return true;
} else {
return false;
}
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.reloadDataForListener(this);
}
}
/**
* To zoom in.
*/
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* To zoom out.
*/
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* Runs a runnable on the UI thread if possible.
*
* @param runnable the runnable
*/
private void runOnUiThread(Runnable runnable) {
FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity != null) {
fragmentActivity.runOnUiThread(runnable);
}
}
/**
* Given a location, fill in a data point, an array of double[]. <br>
* data[0] = time/distance <br>
* data[1] = elevation <br>
* data[2] = speed <br>
* data[3] = pace <br>
* data[4] = heart rate <br>
* data[5] = cadence <br>
* data[6] = power <br>
*
* @param location the location
* @param data the data point to fill in, can be null
*/
@VisibleForTesting
void fillDataPoint(Location location, double[] data) {
double timeOrDistance = Double.NaN;
double elevation = Double.NaN;
double speed = Double.NaN;
double pace = Double.NaN;
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
TripStatistics tripStatistics = tripStatisticsUpdater.getTripStatistics();
if (chartByDistance) {
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
} }
timeOrDistance = distance; return false;
} else {
timeOrDistance = tripStatistics.getTotalTime();
}
elevation = tripStatisticsUpdater.getSmoothedElevation();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
}
speed = tripStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
}
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
}
} }
if (data != null) { /**
data[0] = timeOrDistance; * Checks the chart settings.
data[1] = elevation; */
data[2] = speed; private void checkChartSettings() {
data[3] = pace; boolean needUpdate = false;
data[4] = heartRate; if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) {
data[5] = cadence; chartByDistance = !chartByDistance;
data[6] = power; chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
needUpdate = true;
}
boolean chartShowSpeed = PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_power_key, PreferencesUtils.CHART_SHOW_POWER_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_cadence_key, PreferencesUtils.CHART_SHOW_CADENCE_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_heart_rate_key, PreferencesUtils.CHART_SHOW_HEART_RATE_DEFAULT))) {
needUpdate = true;
}
if (needUpdate) {
chartView.postInvalidate();
}
} }
}
@VisibleForTesting /**
void setTripStatisticsUpdater(long time) { * Sets the series enabled value.
tripStatisticsUpdater = new TripStatisticsUpdater(time); *
} * @param index the series index
* @param value the value
* @return true if changed
*/
private boolean setSeriesEnabled(int index, boolean value) {
if (chartShow[index] != value) {
chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value);
return true;
} else {
return false;
}
}
@VisibleForTesting /**
void setChartView(ChartView view) { * Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
chartView = view; * be accessed by multiple threads.
} */
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
}
@VisibleForTesting /**
void setMetricUnits(boolean value) { * Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
metricUnits = value; * be accessed by multiple threads.
} */
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
@VisibleForTesting /**
void setReportSpeed(boolean value) { * Returns true if the selected track is recording. Needs to be synchronized
reportSpeed = value; * because trackDataHub can be accessed by multiple threads.
} */
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
@VisibleForTesting /**
void setChartByDistance(boolean value) { * Reloads the trackDataHub. Needs to be synchronized because trackDataHub can
chartByDistance = value; * be accessed by multiple threads.
} */
private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.reloadDataForListener(this);
}
}
/**
* To zoom in.
*/
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* To zoom out.
*/
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* Runs a runnable on the UI thread if possible.
*
* @param runnable the runnable
*/
private void runOnUiThread(Runnable runnable) {
FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity != null) {
fragmentActivity.runOnUiThread(runnable);
}
}
/**
* Given a location, fill in a data point, an array of double[]. <br>
* data[0] = time/distance <br>
* data[1] = elevation <br>
* data[2] = speed <br>
* data[3] = pace <br>
* data[4] = heart rate <br>
* data[5] = cadence <br>
* data[6] = power <br>
*
* @param location the location
* @param data the data point to fill in, can be null
*/
@VisibleForTesting
void fillDataPoint(Location location, double[] data) {
double timeOrDistance = Double.NaN;
double elevation = Double.NaN;
double speed = Double.NaN;
double pace = Double.NaN;
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
TripStatistics tripStatistics = tripStatisticsUpdater.getTripStatistics();
if (chartByDistance) {
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else {
timeOrDistance = tripStatistics.getTotalTime();
}
elevation = tripStatisticsUpdater.getSmoothedElevation();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
}
speed = tripStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
}
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
}
}
if (data != null) {
data[0] = timeOrDistance;
data[1] = elevation;
data[2] = speed;
data[3] = pace;
data[4] = heartRate;
data[5] = cadence;
data[6] = power;
}
}
@VisibleForTesting
void setTripStatisticsUpdater(long time) {
tripStatisticsUpdater = new TripStatisticsUpdater(time);
}
@VisibleForTesting
void setChartView(ChartView view) {
chartView = view;
}
@VisibleForTesting
void setMetricUnits(boolean value) {
metricUnits = value;
}
@VisibleForTesting
void setReportSpeed(boolean value) {
reportSpeed = value;
}
@VisibleForTesting
void setChartByDistance(boolean value) {
chartByDistance = value;
}
} }
@@ -46,112 +46,110 @@ import java.util.List;
*/ */
public class ChooseActivityTypeDialogFragment extends DialogFragment { public class ChooseActivityTypeDialogFragment extends DialogFragment {
/** public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
* Interface for caller of this dialog fragment. private static final String KEY_CATEGORY = "category";
* private ChooseActivityTypeCaller caller;
* @author apoorvn
*/ public static ChooseActivityTypeDialogFragment newInstance(String category) {
public interface ChooseActivityTypeCaller { Bundle bundle = new Bundle();
bundle.putString(KEY_CATEGORY, category);
ChooseActivityTypeDialogFragment fragment = new ChooseActivityTypeDialogFragment();
fragment.setArguments(bundle);
return fragment;
}
public static Dialog getDialog(final Activity activity, final String category, final ChooseActivityTypeCaller caller) {
View view = activity.getLayoutInflater().inflate(R.layout.choose_activity_type, null);
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
List<Integer> imageIds = new ArrayList<>();
for (String iconValue : TrackIconUtils.getAllIconValues()) {
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
}
Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(activity.getResources(), R.drawable.ic_track_airplane, options);
int padding = 32;
int width = options.outWidth + 2 * padding;
int height = options.outHeight + 2 * padding;
gridView.setColumnWidth(width);
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(activity, imageIds, width, height, padding);
gridView.setAdapter(imageAdapter);
final AlertDialog alertDialog = new AlertDialog.Builder(activity).setTitle(R.string.track_edit_activity_type_hint).setView(view).create();
alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
int position = getPosition(activity, category);
if (position != -1) {
imageAdapter.setSelected(position);
imageAdapter.notifyDataSetChanged();
}
DialogUtils.setDialogTitleDivider(activity, alertDialog);
}
});
gridView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
caller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position));
alertDialog.dismiss();
}
});
return alertDialog;
}
private static int getPosition(Activity activity, String category) {
if (category == null) {
return -1;
}
String iconValue = TrackIconUtils.getIconValue(activity, category);
if (iconValue.equals("")) {
return -1;
}
List<String> iconValues = TrackIconUtils.getAllIconValues();
for (int i = 0; i < iconValues.size(); i++) {
if (iconValues.get(i).equals(iconValue)) {
return i;
}
}
return -1;
}
private static void updateWeightContainer(View weightContainer, int position) {
boolean showWeight = position == 0 || position == 1 || position == 2;
weightContainer.setVisibility(showWeight ? View.VISIBLE : View.GONE);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (ChooseActivityTypeCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return getDialog(getActivity(), getArguments().getString(KEY_CATEGORY), caller);
}
/** /**
* Called when choose activity type is done. * Interface for caller of this dialog fragment.
*
* @author apoorvn
*/ */
void onChooseActivityTypeDone(String iconValue); public interface ChooseActivityTypeCaller {
}
public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType"; /**
* Called when choose activity type is done.
private static final String KEY_CATEGORY = "category"; */
void onChooseActivityTypeDone(String iconValue);
private ChooseActivityTypeCaller caller;
public static ChooseActivityTypeDialogFragment newInstance(String category) {
Bundle bundle = new Bundle();
bundle.putString(KEY_CATEGORY, category);
ChooseActivityTypeDialogFragment fragment = new ChooseActivityTypeDialogFragment();
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (ChooseActivityTypeCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
} }
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return getDialog(getActivity(), getArguments().getString(KEY_CATEGORY), caller);
}
public static Dialog getDialog(final Activity activity, final String category, final ChooseActivityTypeCaller caller) {
View view = activity.getLayoutInflater().inflate(R.layout.choose_activity_type, null);
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
List<Integer> imageIds = new ArrayList<>();
for (String iconValue : TrackIconUtils.getAllIconValues()) {
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
}
Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(activity.getResources(), R.drawable.ic_track_airplane, options);
int padding = 32;
int width = options.outWidth + 2 * padding;
int height = options.outHeight + 2 * padding;
gridView.setColumnWidth(width);
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(activity, imageIds, width, height, padding);
gridView.setAdapter(imageAdapter);
final AlertDialog alertDialog = new AlertDialog.Builder(activity).setTitle(R.string.track_edit_activity_type_hint).setView(view).create();
alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
int position = getPosition(activity, category);
if (position != -1) {
imageAdapter.setSelected(position);
imageAdapter.notifyDataSetChanged();
}
DialogUtils.setDialogTitleDivider(activity, alertDialog);
}
});
gridView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
caller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position));
alertDialog.dismiss();
}
});
return alertDialog;
}
private static int getPosition(Activity activity, String category) {
if (category == null) {
return -1;
}
String iconValue = TrackIconUtils.getIconValue(activity, category);
if (iconValue.equals("")) {
return -1;
}
List<String> iconValues = TrackIconUtils.getAllIconValues();
for (int i = 0; i < iconValues.size(); i++) {
if (iconValues.get(i).equals(iconValue)) {
return i;
}
}
return -1;
}
private static void updateWeightContainer(View weightContainer, int position) {
boolean showWeight = position == 0 || position == 1 || position == 2;
weightContainer.setVisibility(showWeight ? View.VISIBLE : View.GONE);
}
} }
@@ -16,8 +16,6 @@
package com.google.android.apps.mytracks.fragments; package com.google.android.apps.mytracks.fragments;
import com.google.android.maps.mytracks.R;
import android.content.Context; import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.view.View; import android.view.View;
@@ -25,6 +23,8 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import com.google.android.maps.mytracks.R;
import java.util.List; import java.util.List;
/** /**
@@ -34,62 +34,62 @@ import java.util.List;
*/ */
public class ChooseActivityTypeImageAdapter extends BaseAdapter { public class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final Context context; private final Context context;
private final List<Integer> imageIds; private final List<Integer> imageIds;
private final int width; private final int width;
private final int height; private final int height;
private final int padding; private final int padding;
private int selected = -1; private int selected = -1;
public ChooseActivityTypeImageAdapter( public ChooseActivityTypeImageAdapter(
Context context, List<Integer> imageIds, int width, int height, int padding) { Context context, List<Integer> imageIds, int width, int height, int padding) {
this.context = context; this.context = context;
this.imageIds = imageIds; this.imageIds = imageIds;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.padding = padding; this.padding = padding;
}
@Override
public int getCount() {
return imageIds.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
public int getSelected() {
return selected;
}
public void setSelected(int position) {
selected = position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
if (convertView == null) {
imageView = new ImageView(context);
} else {
imageView = (ImageView) convertView;
} }
if (position == selected) {
imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark); @Override
} else { public int getCount() {
imageView.setBackgroundColor(Color.TRANSPARENT); return imageIds.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
public int getSelected() {
return selected;
}
public void setSelected(int position) {
selected = position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
if (convertView == null) {
imageView = new ImageView(context);
} else {
imageView = (ImageView) convertView;
}
if (position == selected) {
imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark);
} else {
imageView.setBackgroundColor(Color.TRANSPARENT);
}
imageView.setImageResource(imageIds.get(position));
imageView.setMinimumHeight(height);
imageView.setMinimumWidth(width);
imageView.setPadding(padding, padding, padding, padding);
return imageView;
} }
imageView.setImageResource(imageIds.get(position));
imageView.setMinimumHeight(height);
imageView.setMinimumWidth(width);
imageView.setPadding(padding, padding, padding, padding);
return imageView;
}
} }
@@ -31,67 +31,66 @@ import com.google.android.maps.mytracks.R;
*/ */
public class ConfirmDeleteDialogFragment extends AbstractDialogFragment { public class ConfirmDeleteDialogFragment extends AbstractDialogFragment {
/** public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
* Interface for caller of this dialog fragment. private static final String KEY_TRACK_IDS = "trackIds";
* private ConfirmDeleteCaller caller;
* @author Jimmy Shih
*/
public interface ConfirmDeleteCaller {
/** /**
* Called when confirm delete is done. * Create a new instance.
* *
* @param trackIds list of track ids to delete. To delete all, set to size 1 * @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L * with trackIds[0] == -1L
*/ */
void onConfirmDeleteDone(long[] trackIds); public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) {
} Bundle bundle = new Bundle();
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog"; ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
private static final String KEY_TRACK_IDS = "trackIds"; deleteTrackDialogFragment.setArguments(bundle);
return deleteTrackDialogFragment;
/**
* Create a new instance.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L
*/
public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) {
Bundle bundle = new Bundle();
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
deleteTrackDialogFragment.setArguments(bundle);
return deleteTrackDialogFragment;
}
private ConfirmDeleteCaller caller;
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (ConfirmDeleteCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + ConfirmDeleteCaller.class.getSimpleName());
} }
}
@Override @Override
protected Dialog createDialog() { public void onAttach(Context context) {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS); super.onAttach(context);
int titleId; try {
int messageId; caller = (ConfirmDeleteCaller) context;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title } catch (ClassCastException e) {
: R.string.track_delete_one_confirm_title; throw new ClassCastException(context + " must implement " + ConfirmDeleteCaller.class.getSimpleName());
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message }
: R.string.track_delete_one_confirm_message; }
return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() { @Override
@Override protected Dialog createDialog() {
public void onClick(DialogInterface dialog, int which) { final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
caller.onConfirmDeleteDone(trackIds); int titleId;
} int messageId;
}); titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title
} : R.string.track_delete_one_confirm_title;
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message
: R.string.track_delete_one_confirm_message;
return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onConfirmDeleteDone(trackIds);
}
});
}
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ConfirmDeleteCaller {
/**
* Called when confirm delete is done.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L
*/
void onConfirmDeleteDone(long[] trackIds);
}
} }
@@ -23,8 +23,8 @@ import android.os.Bundle;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.util.DialogUtils; import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -35,75 +35,74 @@ import com.google.android.maps.mytracks.R;
*/ */
public class DeleteMarkerDialogFragment extends AbstractDialogFragment { public class DeleteMarkerDialogFragment extends AbstractDialogFragment {
/** public static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog";
* Interface for caller of this dialog fragment. private static final String KEY_MARKER_IDS = "markerIds";
* private DeleteMarkerCaller caller;
* @author Jimmy Shih
*/ public static DeleteMarkerDialogFragment newInstance(long[] markerIds) {
public interface DeleteMarkerCaller { Bundle bundle = new Bundle();
bundle.putLongArray(KEY_MARKER_IDS, markerIds);
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
deleteMarkerDialogFragment.setArguments(bundle);
return deleteMarkerDialogFragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (DeleteMarkerCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DeleteMarkerCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
final FragmentActivity fragmentActivity = getActivity();
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
int titleId;
int messageId;
if (markerIds.length == 1 && markerIds[0] == -1L) {
titleId = R.string.generic_delete_all_confirm_title;
messageId = R.string.marker_delete_all_confirm_message;
} else {
titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title
: R.string.marker_delete_one_confirm_title;
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message
: R.string.marker_delete_one_confirm_message;
}
return DialogUtils.createConfirmationDialog(
fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(new Runnable() {
@Override
public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(
fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity));
}
caller.onDeleteMarkerDone();
}
}).start();
}
});
}
/** /**
* Called when delete marker is done. * Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/ */
void onDeleteMarkerDone(); public interface DeleteMarkerCaller {
}
public static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog"; /**
private static final String KEY_MARKER_IDS = "markerIds"; * Called when delete marker is done.
*/
public static DeleteMarkerDialogFragment newInstance(long[] markerIds) { void onDeleteMarkerDone();
Bundle bundle = new Bundle();
bundle.putLongArray(KEY_MARKER_IDS, markerIds);
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
deleteMarkerDialogFragment.setArguments(bundle);
return deleteMarkerDialogFragment;
}
private DeleteMarkerCaller caller;
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (DeleteMarkerCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DeleteMarkerCaller.class.getSimpleName());
} }
}
@Override
protected Dialog createDialog() {
final FragmentActivity fragmentActivity = getActivity();
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
int titleId;
int messageId;
if (markerIds.length == 1 && markerIds[0] == -1L) {
titleId = R.string.generic_delete_all_confirm_title;
messageId = R.string.marker_delete_all_confirm_message;
} else {
titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title
: R.string.marker_delete_one_confirm_title;
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message
: R.string.marker_delete_one_confirm_message;
}
return DialogUtils.createConfirmationDialog(
fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(new Runnable() {
@Override
public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(
fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity));
}
caller.onDeleteMarkerDone();
}
}).start();
}
});
}
} }
@@ -34,67 +34,65 @@ import com.google.android.maps.mytracks.R;
*/ */
public class FileTypeDialogFragment extends AbstractDialogFragment { public class FileTypeDialogFragment extends AbstractDialogFragment {
/** public static final String FILE_TYPE_DIALOG_TAG = "fileType";
* Interface for caller of this dialog fragment. private static final String KEY_MENU_ID = "menuId";
* private static final String KEY_TITLE_ID = "titleId";
* @author Jimmy Shih private static final String KEY_OPTION_ID = "optionId";
*/ private FileTypeCaller caller;
public interface FileTypeCaller {
public static FileTypeDialogFragment newInstance(int menuId, int titleId, int optionId) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_MENU_ID, menuId);
bundle.putInt(KEY_TITLE_ID, titleId);
bundle.putInt(KEY_OPTION_ID, optionId);
FileTypeDialogFragment fileTypeDialogFragment = new FileTypeDialogFragment();
fileTypeDialogFragment.setArguments(bundle);
return fileTypeDialogFragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (FileTypeCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + FileTypeCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
int optionId = getArguments().getInt(KEY_OPTION_ID);
final int titleId = getArguments().getInt(KEY_TITLE_ID);
final int menuId = getArguments().getInt(KEY_MENU_ID);
TrackFileFormat[] trackFileFormats = TrackFileFormat.values();
String[] choices = new String[trackFileFormats.length];
for (int i = 0; i < choices.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
choices[i] = getString(optionId, trackFileFormat.name(),
FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
}
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
caller.onFileTypeDone(menuId, TrackFileFormat.values()[position]);
}
}).setSingleChoiceItems(choices, 0, null).setTitle(titleId).create();
}
/** /**
* Called when file type selection is done. * Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/ */
void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat); public interface FileTypeCaller {
}
public static final String FILE_TYPE_DIALOG_TAG = "fileType"; /**
* Called when file type selection is done.
private static final String KEY_MENU_ID = "menuId"; */
private static final String KEY_TITLE_ID = "titleId"; void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat);
private static final String KEY_OPTION_ID = "optionId";
public static FileTypeDialogFragment newInstance(int menuId, int titleId, int optionId) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_MENU_ID, menuId);
bundle.putInt(KEY_TITLE_ID, titleId);
bundle.putInt(KEY_OPTION_ID, optionId);
FileTypeDialogFragment fileTypeDialogFragment = new FileTypeDialogFragment();
fileTypeDialogFragment.setArguments(bundle);
return fileTypeDialogFragment;
}
private FileTypeCaller caller;
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
caller = (FileTypeCaller) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + FileTypeCaller.class.getSimpleName());
} }
}
@Override
protected Dialog createDialog() {
int optionId = getArguments().getInt(KEY_OPTION_ID);
final int titleId = getArguments().getInt(KEY_TITLE_ID);
final int menuId = getArguments().getInt(KEY_MENU_ID);
TrackFileFormat[] trackFileFormats = TrackFileFormat.values();
String[] choices = new String[trackFileFormats.length];
for (int i = 0; i < choices.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
choices[i] = getString(optionId, trackFileFormat.name(),
FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
}
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
caller.onFileTypeDone(menuId, TrackFileFormat.values()[position]);
}
}).setSingleChoiceItems(choices, 0, null).setTitle(titleId).create();
}
} }
@@ -16,16 +16,17 @@
package com.google.android.apps.mytracks.fragments; package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
/** /**
* A DialogFragment to configure frequency. * A DialogFragment to configure frequency.
* *
@@ -33,74 +34,74 @@ import androidx.fragment.app.FragmentActivity;
*/ */
public class FrequencyDialogFragment extends AbstractDialogFragment { public class FrequencyDialogFragment extends AbstractDialogFragment {
public static final String FREQUENCY_DIALOG_TAG = "frequencyDialog"; public static final String FREQUENCY_DIALOG_TAG = "frequencyDialog";
private static final String KEY_PREFERENCE_ID = "preferenceId"; private static final String KEY_PREFERENCE_ID = "preferenceId";
private static final String KEY_DEFAULT_VALUE = "defaultValue"; private static final String KEY_DEFAULT_VALUE = "defaultValue";
private static final String KEY_TITLE_ID = "titleId"; private static final String KEY_TITLE_ID = "titleId";
public static FrequencyDialogFragment newInstance( public static FrequencyDialogFragment newInstance(
int preferenceId, int defaultValue, int titleId) { int preferenceId, int defaultValue, int titleId) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(KEY_PREFERENCE_ID, preferenceId); bundle.putInt(KEY_PREFERENCE_ID, preferenceId);
bundle.putInt(KEY_DEFAULT_VALUE, defaultValue); bundle.putInt(KEY_DEFAULT_VALUE, defaultValue);
bundle.putInt(KEY_TITLE_ID, titleId); bundle.putInt(KEY_TITLE_ID, titleId);
FrequencyDialogFragment frequencyDialogFragment = new FrequencyDialogFragment(); FrequencyDialogFragment frequencyDialogFragment = new FrequencyDialogFragment();
frequencyDialogFragment.setArguments(bundle); frequencyDialogFragment.setArguments(bundle);
return frequencyDialogFragment; return frequencyDialogFragment;
}
@Override
protected Dialog createDialog() {
FragmentActivity fragmentActivity = getActivity();
final int preferenceId = getArguments().getInt(KEY_PREFERENCE_ID);
int defaultValue = getArguments().getInt(KEY_DEFAULT_VALUE);
int titleId = getArguments().getInt(KEY_TITLE_ID);
int frequencyValue = PreferencesUtils.getInt(fragmentActivity, preferenceId, defaultValue);
return new AlertDialog.Builder(fragmentActivity).setPositiveButton(
R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int listIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setInt(getActivity(), preferenceId, getFrequencyValue(listIndex));
}
}).setSingleChoiceItems(
getFrequencyDisplayOptions(fragmentActivity), getListIndex(frequencyValue), null)
.setTitle(titleId).create();
}
/**
* Gets the frequency display options.
*/
private String[] getFrequencyDisplayOptions(FragmentActivity fragmentActivity) {
boolean metricUnits = PreferencesUtils.isMetricUnits(fragmentActivity);
return StringUtils.getFrequencyOptions(fragmentActivity, metricUnits);
}
/**
* Gets the list index for a frequency value. Returns 0 if the value is not on
* the list.
*/
private int getListIndex(int frequencyValue) {
String[] values = getResources().getStringArray(R.array.frequency_values);
for (int i = 0; i < values.length; i++) {
if (frequencyValue == Integer.parseInt(values[i])) {
return i;
}
} }
return 0;
}
/** @Override
* Gets the frequency value from a list index. protected Dialog createDialog() {
* FragmentActivity fragmentActivity = getActivity();
* @param listIndex the list index
*/ final int preferenceId = getArguments().getInt(KEY_PREFERENCE_ID);
private int getFrequencyValue(int listIndex) { int defaultValue = getArguments().getInt(KEY_DEFAULT_VALUE);
String[] values = getResources().getStringArray(R.array.frequency_values); int titleId = getArguments().getInt(KEY_TITLE_ID);
return Integer.parseInt(values[listIndex]); int frequencyValue = PreferencesUtils.getInt(fragmentActivity, preferenceId, defaultValue);
}
return new AlertDialog.Builder(fragmentActivity).setPositiveButton(
R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int listIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setInt(getActivity(), preferenceId, getFrequencyValue(listIndex));
}
}).setSingleChoiceItems(
getFrequencyDisplayOptions(fragmentActivity), getListIndex(frequencyValue), null)
.setTitle(titleId).create();
}
/**
* Gets the frequency display options.
*/
private String[] getFrequencyDisplayOptions(FragmentActivity fragmentActivity) {
boolean metricUnits = PreferencesUtils.isMetricUnits(fragmentActivity);
return StringUtils.getFrequencyOptions(fragmentActivity, metricUnits);
}
/**
* Gets the list index for a frequency value. Returns 0 if the value is not on
* the list.
*/
private int getListIndex(int frequencyValue) {
String[] values = getResources().getStringArray(R.array.frequency_values);
for (int i = 0; i < values.length; i++) {
if (frequencyValue == Integer.parseInt(values[i])) {
return i;
}
}
return 0;
}
/**
* Gets the frequency value from a list index.
*
* @param listIndex the list index
*/
private int getFrequencyValue(int listIndex) {
String[] values = getResources().getStringArray(R.array.frequency_values);
return Integer.parseInt(values[listIndex]);
}
} }
@@ -57,231 +57,231 @@ import com.google.android.maps.mytracks.R;
*/ */
public class MarkerDetailFragment extends Fragment { public class MarkerDetailFragment extends Fragment {
public static MarkerDetailFragment newInstance(long markerId, String title) { private static final String TAG = MarkerDetailFragment.class.getSimpleName();
Bundle bundle = new Bundle(); private static final String KEY_MARKER_ID = "markerId";
bundle.putLong(KEY_MARKER_ID, markerId); private static final String KEY_TITLE = "title";
bundle.putString(KEY_TITLE, title); private static final long HIDE_TEXT_DELAY = 4000L; // 4 seconds
private ContentProviderUtils contentProviderUtils;
MarkerDetailFragment fragment = new MarkerDetailFragment(); private Handler handler;
fragment.setArguments(bundle); private ImageView photo;
return fragment; private ImageView textGradient;
} private LinearLayout waypointInfo;
private Waypoint waypoint;
private static final String TAG = MarkerDetailFragment.class.getSimpleName(); private Runnable hideText = new Runnable() {
private static final String KEY_MARKER_ID = "markerId";
private static final String KEY_TITLE = "title";
private static final long HIDE_TEXT_DELAY = 4000L; // 4 seconds
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private ImageView photo;
private ImageView textGradient;
private LinearLayout waypointInfo;
private Waypoint waypoint;
private Runnable hideText = new Runnable() {
@Override
public void run() {
Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadeout);
animation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation anim) {}
@Override
public void onAnimationRepeat(Animation anim) {}
@Override
public void onAnimationEnd(Animation anim) {
textGradient.setVisibility(View.GONE);
waypointInfo.setVisibility(View.GONE);
}
});
textGradient.startAnimation(animation);
waypointInfo.startAnimation(animation);
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
long markerId = getArguments().getLong(KEY_MARKER_ID);
if (markerId == -1L) {
Log.d(TAG, "invalid marker id");
getFragmentManager().popBackStack();
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity());
handler = new Handler();
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
getActivity().setTitle(getArguments().getString(KEY_TITLE));
photo = view.findViewById(R.id.marker_detail_waypoint_photo);
textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient);
waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info);
photo.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void run() {
handler.removeCallbacks(hideText); Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadeout);
int visibility = waypointInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE; animation.setAnimationListener(new AnimationListener() {
textGradient.setVisibility(visibility);
waypointInfo.setVisibility(visibility); @Override
if (visibility == View.VISIBLE) { public void onAnimationStart(Animation anim) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY); }
@Override
public void onAnimationRepeat(Animation anim) {
}
@Override
public void onAnimationEnd(Animation anim) {
textGradient.setVisibility(View.GONE);
waypointInfo.setVisibility(View.GONE);
}
});
textGradient.startAnimation(animation);
waypointInfo.startAnimation(animation);
} }
} };
});
return view; public static MarkerDetailFragment newInstance(long markerId, String title) {
} Bundle bundle = new Bundle();
bundle.putLong(KEY_MARKER_ID, markerId);
bundle.putString(KEY_TITLE, title);
MarkerDetailFragment fragment = new MarkerDetailFragment();
fragment.setArguments(bundle);
return fragment;
}
@Override @Override
public void onResume() { public void onCreate(Bundle savedInstanceState) {
super.onResume(); super.onCreate(savedInstanceState);
// Need to update the waypoint in case returning after an edit long markerId = getArguments().getLong(KEY_MARKER_ID);
updateWaypoint(true); if (markerId == -1L) {
updateUi(); Log.d(TAG, "invalid marker id");
} getFragmentManager().popBackStack();
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity());
handler = new Handler();
setHasOptionsMenu(true);
}
@Override @Override
public void onPause() { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onPause(); View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
handler.removeCallbacks(hideText);
}
@Override getActivity().setTitle(getArguments().getString(KEY_TITLE));
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible); photo = view.findViewById(R.id.marker_detail_waypoint_photo);
/* textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient);
* View pager caches the neighboring fragments in the resumed state. If waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info);
* becoming visible from the resumed state, update the UI to display the
* text above the image. photo.setOnClickListener(new View.OnClickListener() {
*/ @Override
if (isResumed()) { public void onClick(View v) {
if (menuVisible) { handler.removeCallbacks(hideText);
int visibility = waypointInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
textGradient.setVisibility(visibility);
waypointInfo.setVisibility(visibility);
if (visibility == View.VISIBLE) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
// Need to update the waypoint in case returning after an edit
updateWaypoint(true);
updateUi(); updateUi();
} else { }
@Override
public void onPause() {
super.onPause();
handler.removeCallbacks(hideText); handler.removeCallbacks(hideText);
}
} }
}
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void setMenuVisibility(boolean menuVisible) {
inflater.inflate(R.menu.marker_detail, menu); super.setMenuVisibility(menuVisible);
/*
updateWaypoint(false); * View pager caches the neighboring fragments in the resumed state. If
* becoming visible from the resumed state, update the UI to display the
TrackIconUtils.setMenuIconColor(menu); * text above the image.
} */
if (isResumed()) {
@Override if (menuVisible) {
public boolean onOptionsItemSelected(MenuItem item) { updateUi();
long markerId = getArguments().getLong(KEY_MARKER_ID); } else {
FragmentActivity fragmentActivity = getActivity(); handler.removeCallbacks(hideText);
Intent intent; }
switch (item.getItemId()) { }
case R.id.marker_detail_show_on_map:
intent = IntentUtils.newShowOnMapIntent(waypoint);
startActivity(intent);
return true;
case R.id.marker_detail_edit:
intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.marker_detail_delete:
DeleteMarkerDialogFragment.newInstance(new long[] { markerId })
.show(getChildFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true;
default:
return super.onOptionsItemSelected(item);
} }
}
/** @Override
* Updates the waypoint. public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
* inflater.inflate(R.menu.marker_detail, menu);
* @param refresh true to always update
*/ updateWaypoint(false);
private void updateWaypoint(boolean refresh) {
if (refresh || waypoint == null) { TrackIconUtils.setMenuIconColor(menu);
waypoint = contentProviderUtils.getWaypoint(getArguments().getLong(KEY_MARKER_ID));
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
getFragmentManager().popBackStack();
}
} }
}
/** @Override
* Updates the UI. public boolean onOptionsItemSelected(MenuItem item) {
*/ long markerId = getArguments().getLong(KEY_MARKER_ID);
private void updateUi() { FragmentActivity fragmentActivity = getActivity();
View waypointView = getView().findViewById(R.id.marker_detail_waypoint); Intent intent;
View statisticsView = getView().findViewById(R.id.marker_detail_statistics); switch (item.getItemId()) {
case R.id.marker_detail_show_on_map:
boolean isWaypoint = waypoint.getType() == WaypointType.WAYPOINT; intent = IntentUtils.newShowOnMapIntent(waypoint);
waypointView.setVisibility(isWaypoint ? View.VISIBLE : View.GONE); startActivity(intent);
statisticsView.setVisibility(isWaypoint ? View.GONE : View.VISIBLE); return true;
case R.id.marker_detail_edit:
if (isWaypoint) { intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class)
boolean hasPhoto = waypoint.hasPhoto(); .putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
photo.setVisibility(hasPhoto ? View.VISIBLE : View.GONE); startActivity(intent);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE); return true;
waypointInfo.setVisibility(View.VISIBLE); case R.id.marker_detail_delete:
DeleteMarkerDialogFragment.newInstance(new long[]{markerId})
if (hasPhoto) { .show(getChildFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
handler.removeCallbacks(hideText); return true;
photo.setImageURI(waypoint.getPhotoURI()); default:
handler.postDelayed(hideText, HIDE_TEXT_DELAY); return super.onOptionsItemSelected(item);
} }
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
} else {
setName(R.id.marker_detail_statistics_name, false);
setLocation(R.id.marker_detail_statistics_location, false);
Track track = contentProviderUtils.getTrack(waypoint.getTrackId());
StatsUtils.setTripStatisticsValues(getActivity(), null, getView(), waypoint.getTripStatistics(), null);
StatsUtils.setLocationValues(getActivity(), null, getView(), waypoint.getLocation(), false);
} }
}
private void setName(int resId, boolean addShadow) { /**
TextView textView = getView().findViewById(resId); * Updates the waypoint.
ListItemUtils.setTextView(getActivity(), textView, waypoint.getName(), addShadow); *
} * @param refresh true to always update
*/
private void setLocation(int resId, boolean addShadow) { private void updateWaypoint(boolean refresh) {
TextView textView = getView().findViewById(resId); if (refresh || waypoint == null) {
Location location = waypoint.getLocation(); waypoint = contentProviderUtils.getWaypoint(getArguments().getLong(KEY_MARKER_ID));
String value; if (waypoint == null) {
if (location == null) { Log.d(TAG, "waypoint is null");
value = null; getFragmentManager().popBackStack();
} else { }
value = "[" + getString(R.string.stats_latitude) + " " }
+ StringUtils.formatCoordinate(location.getLatitude()) + ", " }
+ getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(location.getLongitude()) + "]"; /**
* Updates the UI.
*/
private void updateUi() {
View waypointView = getView().findViewById(R.id.marker_detail_waypoint);
View statisticsView = getView().findViewById(R.id.marker_detail_statistics);
boolean isWaypoint = waypoint.getType() == WaypointType.WAYPOINT;
waypointView.setVisibility(isWaypoint ? View.VISIBLE : View.GONE);
statisticsView.setVisibility(isWaypoint ? View.GONE : View.VISIBLE);
if (isWaypoint) {
boolean hasPhoto = waypoint.hasPhoto();
photo.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
waypointInfo.setVisibility(View.VISIBLE);
if (hasPhoto) {
handler.removeCallbacks(hideText);
photo.setImageURI(waypoint.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
} else {
setName(R.id.marker_detail_statistics_name, false);
setLocation(R.id.marker_detail_statistics_location, false);
Track track = contentProviderUtils.getTrack(waypoint.getTrackId());
StatsUtils.setTripStatisticsValues(getActivity(), null, getView(), waypoint.getTripStatistics(), null);
StatsUtils.setLocationValues(getActivity(), null, getView(), waypoint.getLocation(), false);
}
}
private void setName(int resId, boolean addShadow) {
TextView textView = getView().findViewById(resId);
ListItemUtils.setTextView(getActivity(), textView, waypoint.getName(), addShadow);
}
private void setLocation(int resId, boolean addShadow) {
TextView textView = getView().findViewById(resId);
Location location = waypoint.getLocation();
String value;
if (location == null) {
value = null;
} else {
value = "[" + getString(R.string.stats_latitude) + " "
+ StringUtils.formatCoordinate(location.getLatitude()) + ", "
+ getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(location.getLongitude()) + "]";
}
ListItemUtils.setTextView(getActivity(), textView, value, addShadow);
} }
ListItemUtils.setTextView(getActivity(), textView, value, addShadow);
}
} }
@@ -59,268 +59,266 @@ import java.util.EnumSet;
*/ */
public class StatsFragment extends Fragment implements TrackDataListener { public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = "statsFragment"; public static final String STATS_FRAGMENT_TAG = "statsFragment";
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private Handler handlerUpdateUI; private Handler handlerUpdateUI;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Location lastLocation = null; private Location lastLocation = null;
private TripStatistics lastTripStatistics = null; private TripStatistics lastTripStatistics = null;
private SensorDataSet sensorDataSet = null; private SensorDataSet sensorDataSet = null;
private String category = ""; private final Runnable updateUIeachSecond = new Runnable() {
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT; public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
updateSensorDataUI();
}
private final Runnable updateUIeachSecond = new Runnable() { handlerUpdateUI.postDelayed(this, UnitConversions.ONE_SECOND);
public void run() { }
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
updateSensorDataUI();
} }
};
private String category = "";
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
handlerUpdateUI.postDelayed(this, UnitConversions.ONE_SECOND); @Override
} public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.stats, container, false);
} }
};
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
handlerUpdateUI = new Handler();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
TrackRecordingServiceConnectionUtils.startConnection(getContext(), trackRecordingServiceConnection);
@Override Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
return inflater.inflate(R.layout.stats, container, false); activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
} @Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackDetailActivity) getActivity()).chooseActivityType(category);
}
return true;
}
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackDetailActivity) getActivity()).chooseActivityType(category);
}
return true;
}
});
}
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onResume() {
super.onActivityCreated(savedInstanceState); super.onResume();
handlerUpdateUI = new Handler(); resumeTrackDataHub();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null); updateUi(getActivity());
TrackRecordingServiceConnectionUtils.startConnection(getContext(), trackRecordingServiceConnection); if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond);
}
}
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon); @Override
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), "")); public void onPause() {
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() { super.onPause();
@Override pauseTrackDataHub();
public boolean onTouch(View v, MotionEvent event) { handlerUpdateUI.removeCallbacks(updateUIeachSecond);
if (event.getAction() == MotionEvent.ACTION_UP) { }
((TrackDetailActivity) getActivity()).chooseActivityType(category);
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
}
@Override
public void onTrackUpdated(final Track track) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null;
category = track != null ? track.getCategory() : "";
updateUi(getActivity());
}
}
});
}
}
@Override
public void clearTrackPoints() {
lastLocation = null;
}
@Override
public void onSampledInTrackPoint(Location location) {
lastLocation = location;
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastLocation = location;
}
@Override
public void onSegmentSplit(Location location) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastLocation = null;
}
if (lastLocation != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastLocation);
boolean hasGoodFix = lastLocation.hasAccuracy() && lastLocation.getAccuracy() < recordingGpsAccuracy;
if (!hasFix || !hasGoodFix) {
lastLocation = null;
}
}
StatsUtils.setLocationValues(getActivity(), getActivity(), null, lastLocation, isSelectedTrackRecording());
}
}
});
}
}
@Override
public void clearWaypoints() {
// We don't care.
}
@Override
public void onNewWaypoint(Waypoint wpt) {
// We don't care.
}
@Override
public void onNewWaypointsDone() {
// We don't care.
}
@Override
public boolean onMetricUnitsChanged(final boolean metric) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
}
}
});
} }
return true; return true;
} }
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() { @Override
@Override public boolean onReportSpeedChanged(final boolean speed) {
public boolean onKey(View v, int keyCode, KeyEvent event) { if (isResumed()) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { getActivity().runOnUiThread(new Runnable() {
((TrackDetailActivity) getActivity()).chooseActivityType(category); @Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
}
}
});
} }
return true; return true;
}
});
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
updateUi(getActivity());
if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond);
} }
}
@Override @Override
public void onPause() { public boolean onRecordingGpsAccuracy(int newValue) {
super.onPause(); recordingGpsAccuracy = newValue;
pauseTrackDataHub(); return false;
handlerUpdateUI.removeCallbacks(updateUIeachSecond); }
}
@Override @Override
public void onStop() { public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
super.onStop(); // We don't care.
trackRecordingServiceConnection.unbind(); return false;
} }
@Override /**
public void onTrackUpdated(final Track track) { * Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
if (isResumed()) { * be accessed by multiple threads.
getActivity().runOnUiThread(new Runnable() { */
@Override private synchronized void resumeTrackDataHub() {
public void run() { trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
if (isResumed()) { trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
lastTripStatistics = track != null ? track.getTripStatistics() : null; TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
category = track != null ? track.getCategory() : ""; TrackDataType.PREFERENCE));
updateUi(getActivity()); }
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Returns true if the selected track is paused. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackPaused() {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
/**
* Tries to fetch most recent {@link SensorDataSet} {@link com.google.android.apps.mytracks.services.sensors.SensorManager}.
*/
private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService == null) {
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
sensorDataSet = null;
} else {
//TODO sensorState = trackRecordingService.getSensorState();
sensorDataSet = trackRecordingService.getSensorData();
} }
});
}
}
@Override StatsUtils.setSensorData(getActivity(), getActivity(), sensorDataSet, isSelectedTrackRecording());
public void clearTrackPoints() {
lastLocation = null;
}
@Override
public void onSampledInTrackPoint(Location location) {
lastLocation = location;
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastLocation = location;
}
@Override
public void onSegmentSplit(Location location) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastLocation = null;
}
if (lastLocation != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastLocation);
boolean hasGoodFix = lastLocation.hasAccuracy() && lastLocation.getAccuracy() < recordingGpsAccuracy;
if (!hasFix || !hasGoodFix) {
lastLocation = null;
}
}
StatsUtils.setLocationValues(getActivity(), getActivity(), null, lastLocation, isSelectedTrackRecording());
}
}
});
}
}
@Override
public void clearWaypoints() {
// We don't care.
}
@Override
public void onNewWaypoint(Waypoint wpt) {
// We don't care.
}
@Override
public void onNewWaypointsDone() {
// We don't care.
}
@Override
public boolean onMetricUnitsChanged(final boolean metric) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
}
}
});
}
return true;
}
@Override
public boolean onReportSpeedChanged(final boolean speed) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
}
}
});
}
return true;
}
@Override
public boolean onRecordingGpsAccuracy(int newValue) {
recordingGpsAccuracy = newValue;
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
// We don't care.
return false;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Returns true if the selected track is paused. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackPaused() {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
/**
* Tries to fetch most recent {@link SensorDataSet} {@link com.google.android.apps.mytracks.services.sensors.SensorManager}.
*/
private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService == null) {
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
sensorDataSet = null;
} else {
//TODO sensorState = trackRecordingService.getSensorState();
sensorDataSet = trackRecordingService.getSensorData();
} }
StatsUtils.setSensorData(getActivity(), getActivity(), sensorDataSet, isSelectedTrackRecording()); /**
} * Updates the UI.
*/
/** private void updateUi(FragmentActivity activity) {
* Updates the UI. String trackIconValue = TrackIconUtils.getIconValue(activity, category);
*/ StatsUtils.setTripStatisticsValues(activity, activity, null, lastTripStatistics, trackIconValue);
private void updateUi(FragmentActivity activity) { StatsUtils.setLocationValues(activity, activity, null, lastLocation, isSelectedTrackRecording());
String trackIconValue = TrackIconUtils.getIconValue(activity, category); updateSensorDataUI();
StatsUtils.setTripStatisticsValues(activity, activity, null, lastTripStatistics, trackIconValue); }
StatsUtils.setLocationValues(activity, activity, null, lastLocation, isSelectedTrackRecording());
updateSensorDataUI();
}
} }
@@ -1,75 +1,75 @@
package com.google.android.apps.mytracks.io.file; package com.google.android.apps.mytracks.io.file;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.apps.mytracks.io.file.exporter.GpxTrackWriter; import com.google.android.apps.mytracks.io.file.exporter.GpxTrackWriter;
import com.google.android.apps.mytracks.io.file.exporter.KmlTrackWriter; import com.google.android.apps.mytracks.io.file.exporter.KmlTrackWriter;
import com.google.android.apps.mytracks.io.file.exporter.TrackWriter; import com.google.android.apps.mytracks.io.file.exporter.TrackWriter;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Locale; import java.util.Locale;
/** /**
* Definition of all possible track formats. * Definition of all possible track formats.
*/ */
public enum TrackFileFormat implements Parcelable { public enum TrackFileFormat implements Parcelable {
KML { KML {
@Override @Override
public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) { public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) {
return new KmlTrackWriter(context, multiple, playTrack); return new KmlTrackWriter(context, multiple, playTrack);
} }
}, },
GPX { GPX {
@Override @Override
public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) { public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) {
return new GpxTrackWriter(context.getString(R.string.app_name)); return new GpxTrackWriter(context.getString(R.string.app_name));
} }
}; };
@Override public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() {
public int describeContents() { @Override
return 0; public TrackFileFormat createFromParcel(final Parcel source) {
} return TrackFileFormat.values()[source.readInt()];
}
@Override @Override
public void writeToParcel(final Parcel dest, final int flags) { public TrackFileFormat[] newArray(final int size) {
dest.writeInt(ordinal()); return new TrackFileFormat[size];
} }
};
public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() { @Override
@Override public int describeContents() {
public TrackFileFormat createFromParcel(final Parcel source) { return 0;
return TrackFileFormat.values()[source.readInt()];
} }
@Override @Override
public TrackFileFormat[] newArray(final int size) { public void writeToParcel(final Parcel dest, final int flags) {
return new TrackFileFormat[size]; dest.writeInt(ordinal());
} }
};
/** /**
* Creates a new track writer for the format. * Creates a new track writer for the format.
* *
* @param context the context * @param context the context
* @param multiple true for writing multiple tracks * @param multiple true for writing multiple tracks
* @param playTrack true to play track * @param playTrack true to play track
*/ */
public abstract TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack); public abstract TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack);
/** /**
* Returns the mime type for each format. * Returns the mime type for each format.
*/ */
public String getMimeType() { public String getMimeType() {
return "application/" + getExtension() + "+xml"; return "application/" + getExtension() + "+xml";
} }
/** /**
* Returns the file extension for each format. * Returns the file extension for each format.
*/ */
public String getExtension() { public String getExtension() {
return this.name().toLowerCase(Locale.US); return this.name().toLowerCase(Locale.US);
} }
} }
@@ -16,18 +16,18 @@
package com.google.android.apps.mytracks.io.file.exporter; package com.google.android.apps.mytracks.io.file.exporter;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.ContentProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.LocationUtils;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.ContentProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.LocationUtils;
import java.io.OutputStream; import java.io.OutputStream;
/** /**
@@ -38,197 +38,197 @@ import java.io.OutputStream;
*/ */
public class FileTrackExporter implements TrackExporter { public class FileTrackExporter implements TrackExporter {
private static final String TAG = FileTrackExporter.class.getSimpleName(); private static final String TAG = FileTrackExporter.class.getSimpleName();
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
private final Track[] tracks; private final Track[] tracks;
private final TrackWriter trackWriter; private final TrackWriter trackWriter;
private final TrackExporterListener trackExporterListener; private final TrackExporterListener trackExporterListener;
/** /**
* Constructor. * Constructor.
* *
* @param contentProviderUtils the content provider utils * @param contentProviderUtils the content provider utils
* @param tracks the tracks * @param tracks the tracks
* @param trackWriter the track writer * @param trackWriter the track writer
* @param trackExporterListener the track export listener * @param trackExporterListener the track export listener
*/
public FileTrackExporter(ContentProviderUtils contentProviderUtils, Track[] tracks,
TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
this.contentProviderUtils = contentProviderUtils;
this.tracks = tracks;
this.trackWriter = trackWriter;
this.trackExporterListener = trackExporterListener;
}
@Override
public boolean writeTrack(OutputStream outputStream) {
try {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks);
for (Track track1 : tracks) {
writeWaypoints(track1);
}
trackWriter.writeBeginTracks();
long startTime = tracks[0].getTripStatistics().getStartTime();
for (Track track : tracks) {
long offset = track.getTripStatistics().getStartTime() - startTime;
writeLocations(track, offset);
}
trackWriter.writeEndTracks();
trackWriter.writeFooter();
trackWriter.close();
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Thread interrupted", e);
return false;
}
}
/**
* Writes the waypoints.
*/
private void writeWaypoints(Track track) throws InterruptedException {
/*
* TODO: Stream through the waypoints in chunks. I am leaving the number of
* waypoints very high which should not be a problem because we don't try to
* load them into objects all at the same time.
*/ */
boolean hasWaypoints = false; public FileTrackExporter(ContentProviderUtils contentProviderUtils, Track[] tracks,
try (Cursor cursor = contentProviderUtils.getWaypointCursor( TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS)) { this.contentProviderUtils = contentProviderUtils;
if (cursor != null && cursor.moveToFirst()) { this.tracks = tracks;
/* this.trackWriter = trackWriter;
* Yes, this will skip the first waypoint and that is intentional as the this.trackExporterListener = trackExporterListener;
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (!hasWaypoints) {
trackWriter.writeBeginWaypoints(track);
hasWaypoints = true;
}
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
trackWriter.writeWaypoint(waypoint);
}
}
} }
if (hasWaypoints) {
trackWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
int locationNumber = 0;
LocationIterator locationIterator = null;
try {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, locationFactory);
while (locationIterator.hasNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Location location = locationIterator.next();
setLocationTime(location, offset);
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackWriter.writeLocation(location);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteTrack) {
Location lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
// Write an empty track
trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null);
}
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* Sets a location time.
*
* @param location the location
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory implements ContentProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override @Override
public Location createLocation() { public boolean writeTrack(OutputStream outputStream) {
if (currentLocation == null) { try {
currentLocation = new SensorDataSetLocation(""); trackWriter.prepare(outputStream);
} trackWriter.writeHeader(tracks);
return currentLocation; for (Track track1 : tracks) {
writeWaypoints(track1);
}
trackWriter.writeBeginTracks();
long startTime = tracks[0].getTripStatistics().getStartTime();
for (Track track : tracks) {
long offset = track.getTripStatistics().getStartTime() - startTime;
writeLocations(track, offset);
}
trackWriter.writeEndTracks();
trackWriter.writeFooter();
trackWriter.close();
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Thread interrupted", e);
return false;
}
} }
public void swapLocations() { /**
Location tempLocation = lastLocation; * Writes the waypoints.
lastLocation = currentLocation; */
currentLocation = tempLocation; private void writeWaypoints(Track track) throws InterruptedException {
if (currentLocation != null) { /*
currentLocation.reset(); * TODO: Stream through the waypoints in chunks. I am leaving the number of
} * waypoints very high which should not be a problem because we don't try to
* load them into objects all at the same time.
*/
boolean hasWaypoints = false;
try (Cursor cursor = contentProviderUtils.getWaypointCursor(
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS)) {
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (!hasWaypoints) {
trackWriter.writeBeginWaypoints(track);
hasWaypoints = true;
}
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
trackWriter.writeWaypoint(waypoint);
}
}
}
if (hasWaypoints) {
trackWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
int locationNumber = 0;
LocationIterator locationIterator = null;
try {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, locationFactory);
while (locationIterator.hasNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Location location = locationIterator.next();
setLocationTime(location, offset);
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackWriter.writeLocation(location);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteTrack) {
Location lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
// Write an empty track
trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null);
}
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* Sets a location time.
*
* @param location the location
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory implements ContentProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new SensorDataSetLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
}
} }
}
} }
@@ -36,166 +36,167 @@ import java.util.Locale;
//TODO Can we export Sensor.Data in GPX? //TODO Can we export Sensor.Data in GPX?
public class GpxTrackWriter implements TrackWriter { public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US); private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US); private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
static {
/*
* GPX readers expect to see fractional numbers with US-style punctuation.
* That is, they want periods for decimal points, rather than commas.
*/
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(6); static {
COORDINATE_FORMAT.setMaximumIntegerDigits(3); /*
COORDINATE_FORMAT.setGroupingUsed(false); * GPX readers expect to see fractional numbers with US-style punctuation.
} * That is, they want periods for decimal points, rather than commas.
*/
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
private final String creator; COORDINATE_FORMAT.setMaximumFractionDigits(6);
private PrintWriter printWriter; COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
public GpxTrackWriter(String creator) {
this.creator = creator;
}
@Override
public String getExtension() {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
} }
}
@Override private final String creator;
public void writeHeader(Track[] tracks) { private PrintWriter printWriter;
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); public GpxTrackWriter(String creator) {
printWriter.println("<gpx"); this.creator = creator;
printWriter.println("version=\"1.1\"");
printWriter.println(
"creator=\"" + creator + "\"");
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println(
"xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("<metadata>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
} }
}
@Override @Override
public void writeFooter() { public String getExtension() {
if (printWriter != null) { return TrackFileFormat.GPX.getExtension();
printWriter.println("</gpx>");
} }
}
@Override @Override
public void writeBeginWaypoints(Track track) { public void prepare(OutputStream outputStream) {
// Do nothing this.printWriter = new PrintWriter(outputStream);
} }
@Override @Override
public void writeEndWaypoints() { public void close() {
// Do nothing if (printWriter != null) {
} printWriter.flush();
printWriter = null;
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
} }
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<cmt>" + StringUtils.formatCData(waypoint.getType().name()) + "</cmt>");
printWriter.println(
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
}
} }
}
@Override @Override
public void writeBeginTracks() { public void writeHeader(Track[] tracks) {
// Do nothing if (printWriter != null) {
} printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<gpx");
@Override printWriter.println("version=\"1.1\"");
public void writeEndTracks() { printWriter.println(
// Do nothing "creator=\"" + creator + "\"");
} printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println(
@Override "xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
public void writeBeginTrack(Track track, Location startLocation) { printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
if (printWriter != null) { printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
printWriter.println("<trk>"); + " http://www.topografix.com/GPX/1/1/gpx.xsd"
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>"); + " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>"); + " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("<type>" + StringUtils.formatCData(track.getCategory()) + "</type>"); printWriter.println("<metadata>");
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>"); Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
}
} }
}
@Override @Override
public void writeEndTrack(Track track, Location endLocation) { public void writeFooter() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("</trk>"); printWriter.println("</gpx>");
}
} }
}
@Override @Override
public void writeOpenSegment() { public void writeBeginWaypoints(Track track) {
printWriter.println("<trkseg>"); // Do nothing
}
@Override
public void writeCloseSegment() {
printWriter.println("</trkseg>");
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
} }
}
/** @Override
* Formats a location with latitude and longitude coordinates. public void writeEndWaypoints() {
* // Do nothing
* @param location the location }
*/
private String formatLocation(Location location) { @Override
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\"" public void writeWaypoint(Waypoint waypoint) {
+ COORDINATE_FORMAT.format(location.getLongitude()) + "\""; if (printWriter != null) {
} Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<cmt>" + StringUtils.formatCData(waypoint.getType().name()) + "</cmt>");
printWriter.println(
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
}
}
}
@Override
public void writeBeginTracks() {
// Do nothing
}
@Override
public void writeEndTracks() {
// Do nothing
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(track.getCategory()) + "</type>");
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
if (printWriter != null) {
printWriter.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
printWriter.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
printWriter.println("</trkseg>");
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
/**
* Formats a location with latitude and longitude coordinates.
*
* @param location the location
*/
private String formatLocation(Location location) {
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\""
+ COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
}
} }
@@ -22,10 +22,10 @@ import android.net.Uri;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.DescriptionGenerator; import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl; import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.SensorDataSetLocation; import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType; import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
@@ -46,415 +46,415 @@ import java.util.ArrayList;
*/ */
public class KmlTrackWriter implements TrackWriter { public class KmlTrackWriter implements TrackWriter {
private static final String WAYPOINT_STYLE = "waypoint"; private static final String WAYPOINT_STYLE = "waypoint";
private static final String STATISTICS_STYLE = "statistics"; private static final String STATISTICS_STYLE = "statistics";
private static final String START_STYLE = "start"; private static final String START_STYLE = "start";
private static final String END_STYLE = "end"; private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track"; private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema"; private static final String SCHEMA_ID = "schema";
private static final String CADENCE = "cadence"; private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate"; private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power"; private static final String POWER = "power";
private static final String private static final String
WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png"; WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String private static final String
STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png"; STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
private static final String private static final String
START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png"; START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String private static final String
END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png"; END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String private static final String
TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png"; TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context; private final Context context;
private final boolean multiple; private final boolean multiple;
private final boolean playTrack; private final boolean playTrack;
private final DescriptionGenerator descriptionGenerator; private final DescriptionGenerator descriptionGenerator;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter; private PrintWriter printWriter;
private ArrayList<Float> powerList = new ArrayList<>(); private ArrayList<Float> powerList = new ArrayList<>();
private ArrayList<Float> cadenceList = new ArrayList<>(); private ArrayList<Float> cadenceList = new ArrayList<>();
private ArrayList<Float> heartRateList = new ArrayList<>(); private ArrayList<Float> heartRateList = new ArrayList<>();
private boolean hasPower; private boolean hasPower;
private boolean hasCadence; private boolean hasCadence;
private boolean hasHeartRate; private boolean hasHeartRate;
public KmlTrackWriter(Context context, boolean multiple, boolean playTrack) { public KmlTrackWriter(Context context, boolean multiple, boolean playTrack) {
this(context, multiple, playTrack, new DescriptionGeneratorImpl(context)); this(context, multiple, playTrack, new DescriptionGeneratorImpl(context));
}
@VisibleForTesting
KmlTrackWriter(Context context, boolean multiple, boolean playTrack, DescriptionGenerator descriptionGenerator) {
this.context = context;
this.multiple = multiple;
this.playTrack = playTrack;
this.descriptionGenerator = descriptionGenerator;
this.contentProviderUtils = ContentProviderUtils.Factory.get(context);
}
@Override
public String getExtension() {
return TrackFileFormat.KML.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
} }
}
@Override @VisibleForTesting
public void writeHeader(Track[] tracks) { KmlTrackWriter(Context context, boolean multiple, boolean playTrack, DescriptionGenerator descriptionGenerator) {
if (printWriter != null) { this.context = context;
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); this.multiple = multiple;
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\""); this.playTrack = playTrack;
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\""); this.descriptionGenerator = descriptionGenerator;
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\">"); this.contentProviderUtils = ContentProviderUtils.Factory.get(context);
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>"
+ StringUtils.formatCData(context.getString(R.string.app_name))
+ "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
printWriter.println("</Schema>");
} }
}
@Override @Override
public void writeFooter() { public String getExtension() {
if (printWriter != null) { return TrackFileFormat.KML.getExtension();
printWriter.println("</Document>");
printWriter.println("</kml>");
} }
}
@Override @Override
public void writeBeginWaypoints(Track track) { public void prepare(OutputStream outputStream) {
if (printWriter != null) { this.printWriter = new PrintWriter(outputStream);
printWriter.println("<Folder><name>"
+ StringUtils.formatCData(context.getString(R.string.track_markers, track.getName()))
+ "</name>");
printWriter.println("<open>1</open>");
} }
}
@Override @Override
public void writeEndWaypoints() { public void close() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("</Folder>"); printWriter.flush();
} printWriter = null;
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
String styleName = waypoint.getType() == WaypointType.STATISTICS ? STATISTICS_STYLE : WAYPOINT_STYLE;
if (waypoint.hasPhoto()) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), styleName, waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), styleName, waypoint.getLocation());
}
}
}
@Override
public void writeBeginTracks() {
if (printWriter != null && multiple) {
printWriter.println("<Folder id=tour>");
printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>");
}
}
@Override
public void writeEndTracks() {
if (printWriter != null && multiple) {
printWriter.println("</Folder>");
}
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, "", "", START_STYLE, startLocation);
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
}
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, null, null, false);
writePlacemark(name, "", description, END_STYLE, endLocation);
}
}
@Override
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<gx:Track>");
hasPower = false;
hasCadence = false;
hasHeartRate = false;
powerList.clear();
cadenceList.clear();
heartRateList.clear();
}
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println(
"<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (location instanceof SensorDataSetLocation) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
}
if (sensorDataSet.hasCadence()) {
cadenceList.add(sensorDataSet.getCadence());
}
if (sensorDataSet.hasPower()) {
powerList.add(sensorDataSet.getPower());
}
} }
}
} }
}
/** @Override
* Writes the sensor data. public void writeHeader(Track[] tracks) {
* if (printWriter != null) {
* @param list a list of sensor data printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
* @param name the name of the sensor data printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
*/ printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
private void writeSensorData(ArrayList<Float> list, String name) { printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\">");
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">"); printWriter.println("<Document>");
for (int i = 0; i < list.size(); i++) { printWriter.println("<open>1</open>");
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>"); printWriter.println("<visibility>1</visibility>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>"
+ StringUtils.formatCData(context.getString(R.string.app_name))
+ "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
printWriter.println("</Schema>");
}
} }
printWriter.println("</gx:SimpleArrayData>");
}
/** @Override
* Writes a placemark. public void writeFooter() {
* if (printWriter != null) {
* @param name the name printWriter.println("</Document>");
* @param category the category printWriter.println("</kml>");
* @param description the description }
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(
String name, String category, String description, String styleName, Location location) {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</Placemark>");
} }
}
/** @Override
* Writes a photo overlay. public void writeBeginWaypoints(Track track) {
* if (printWriter != null) {
* @param name the name printWriter.println("<Folder><name>"
* @param category the category + StringUtils.formatCData(context.getString(R.string.track_markers, track.getName()))
* @param description the description + "</name>");
* @param styleName the style name printWriter.println("<open>1</open>");
* @param location the location }
* @param photoUrl the photo url
* @param heading the heading
*/
private void writePhotoOverlay(String name, String category, String description, String styleName,
Location location, String photoUrl, float heading) {
if (location != null) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
printWriter.print("<altitude>20</altitude>");
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
if (playTrack) {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
} else {
Uri uri = Uri.parse(photoUrl);
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar
+ uri.getLastPathSegment() + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
printWriter.print("<near>10</near>");
printWriter.print("<leftFov>-60</leftFov>");
printWriter.print("<rightFov>60</rightFov>");
printWriter.print("<bottomFov>-45</bottomFov>");
printWriter.print("<topFov>45</topFov>");
printWriter.println("</ViewVolume>");
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</PhotoOverlay>");
} }
}
/** @Override
* Gets the heading to a location. public void writeEndWaypoints() {
* if (printWriter != null) {
* @param trackId the track id containing the location printWriter.println("</Folder>");
* @param location the location }
*/
private float getHeading(long trackId, Location location) {
long trackPointId = contentProviderUtils.getTrackPointId(trackId, location);
if (trackPointId == -1L) {
return location.getBearing();
} }
Location viewLocation;
try (Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true)) { @Override
if (cursor == null || cursor.getCount() == 0) { public void writeWaypoint(Waypoint waypoint) {
return location.getBearing(); if (printWriter != null) {
} String styleName = waypoint.getType() == WaypointType.STATISTICS ? STATISTICS_STYLE : WAYPOINT_STYLE;
cursor.moveToPosition(cursor.getCount() - 1); if (waypoint.hasPhoto()) {
viewLocation = contentProviderUtils.createTrackPoint(cursor); float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), styleName, waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), styleName, waypoint.getLocation());
}
}
} }
return viewLocation.bearingTo(location);
}
private String getCoordinates(Location location, String separator) { @Override
StringBuilder builder = new StringBuilder(); public void writeBeginTracks() {
builder.append(location.getLongitude()).append(separator).append(location.getLatitude()); if (printWriter != null && multiple) {
if (location.hasAltitude()) { printWriter.println("<Folder id=tour>");
builder.append(separator).append(location.getAltitude()); printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>");
}
} }
return builder.toString();
}
/** @Override
* Writes the category. public void writeEndTracks() {
* if (printWriter != null && multiple) {
* @param category the category printWriter.println("</Folder>");
*/ }
private void writeCategory(String category) {
if (category == null || category.equals("")) {
return;
} }
printWriter.println("<ExtendedData>");
printWriter.println(
"<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
/** @Override
* Writes the track style. public void writeBeginTrack(Track track, Location startLocation) {
*/ if (printWriter != null) {
private void writeTrackStyle() { String name = context.getString(R.string.marker_label_start, track.getName());
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">"); writePlacemark(name, "", "", START_STYLE, startLocation);
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>"); printWriter.println("<Placemark>");
printWriter.println("<IconStyle>"); printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<scale>1.3</scale>"); printWriter.println(
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>"); "<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("</IconStyle>"); printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
printWriter.println("</Style>"); writeCategory(track.getCategory());
} printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
}
}
/** @Override
* Writes a placemarker style. public void writeEndTrack(Track track, Location endLocation) {
* if (printWriter != null) {
* @param name the name of the style printWriter.println("</gx:MultiTrack>");
* @param url the url of the style icon printWriter.println("</Placemark>");
* @param x the x position of the hotspot String name = context.getString(R.string.marker_label_end, track.getName());
* @param y the y position of the hotspot String description = descriptionGenerator.generateTrackDescription(track, null, null, false);
*/ writePlacemark(name, "", description, END_STYLE, endLocation);
private void writePlacemarkerStyle(String name, String url, int x, int y) { }
printWriter.println("<Style id=\"" + name + "\"><IconStyle>"); }
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println(
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
/** @Override
* Writes a sensor style. public void writeOpenSegment() {
* if (printWriter != null) {
* @param name the name of the sesnor printWriter.println("<gx:Track>");
* @param displayName the sensor display name hasPower = false;
*/ hasCadence = false;
private void writeSensorStyle(String name, String displayName) { hasHeartRate = false;
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">"); powerList.clear();
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>"); cadenceList.clear();
printWriter.println("</gx:SimpleArrayField>"); heartRateList.clear();
} }
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println(
"<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (location instanceof SensorDataSetLocation) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
}
if (sensorDataSet.hasCadence()) {
cadenceList.add(sensorDataSet.getCadence());
}
if (sensorDataSet.hasPower()) {
powerList.add(sensorDataSet.getPower());
}
}
}
}
}
/**
* Writes the sensor data.
*
* @param list a list of sensor data
* @param name the name of the sensor data
*/
private void writeSensorData(ArrayList<Float> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
}
printWriter.println("</gx:SimpleArrayData>");
}
/**
* Writes a placemark.
*
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(
String name, String category, String description, String styleName, Location location) {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</Placemark>");
}
}
/**
* Writes a photo overlay.
*
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
* @param photoUrl the photo url
* @param heading the heading
*/
private void writePhotoOverlay(String name, String category, String description, String styleName,
Location location, String photoUrl, float heading) {
if (location != null) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
printWriter.print("<altitude>20</altitude>");
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
if (playTrack) {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
} else {
Uri uri = Uri.parse(photoUrl);
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar
+ uri.getLastPathSegment() + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
printWriter.print("<near>10</near>");
printWriter.print("<leftFov>-60</leftFov>");
printWriter.print("<rightFov>60</rightFov>");
printWriter.print("<bottomFov>-45</bottomFov>");
printWriter.print("<topFov>45</topFov>");
printWriter.println("</ViewVolume>");
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</PhotoOverlay>");
}
}
/**
* Gets the heading to a location.
*
* @param trackId the track id containing the location
* @param location the location
*/
private float getHeading(long trackId, Location location) {
long trackPointId = contentProviderUtils.getTrackPointId(trackId, location);
if (trackPointId == -1L) {
return location.getBearing();
}
Location viewLocation;
try (Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true)) {
if (cursor == null || cursor.getCount() == 0) {
return location.getBearing();
}
cursor.moveToPosition(cursor.getCount() - 1);
viewLocation = contentProviderUtils.createTrackPoint(cursor);
}
return viewLocation.bearingTo(location);
}
private String getCoordinates(Location location, String separator) {
StringBuilder builder = new StringBuilder();
builder.append(location.getLongitude()).append(separator).append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(separator).append(location.getAltitude());
}
return builder.toString();
}
/**
* Writes the category.
*
* @param category the category
*/
private void writeCategory(String category) {
if (category == null || category.equals("")) {
return;
}
printWriter.println("<ExtendedData>");
printWriter.println(
"<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
/**
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">");
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
printWriter.println("<IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
printWriter.println("</IconStyle>");
printWriter.println("</Style>");
}
/**
* Writes a placemarker style.
*
* @param name the name of the style
* @param url the url of the style icon
* @param x the x position of the hotspot
* @param y the y position of the hotspot
*/
private void writePlacemarkerStyle(String name, String url, int x, int y) {
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println(
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
/**
* Writes a sensor style.
*
* @param name the name of the sesnor
* @param displayName the sensor display name
*/
private void writeSensorStyle(String name, String displayName) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
} }
@@ -16,12 +16,6 @@
package com.google.android.apps.mytracks.io.file.exporter; package com.google.android.apps.mytracks.io.file.exporter;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@@ -29,6 +23,12 @@ import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@@ -43,143 +43,143 @@ import java.util.zip.ZipOutputStream;
*/ */
public class KmzTrackExporter implements TrackExporter { public class KmzTrackExporter implements TrackExporter {
public static final String KMZ_EXTENSION = "kmz"; public static final String KMZ_EXTENSION = "kmz";
public static final String KMZ_IMAGES_DIR = "images"; public static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_KML_FILE = "doc.kml"; public static final String KMZ_KML_FILE = "doc.kml";
private static final String TAG = KmzTrackExporter.class.getSimpleName(); private static final String TAG = KmzTrackExporter.class.getSimpleName();
private static final int BUFFER_SIZE = 4096; private static final int BUFFER_SIZE = 4096;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
private final FileTrackExporter fileTrackExporter; private final FileTrackExporter fileTrackExporter;
private final Track[] tracks; private final Track[] tracks;
private final long photoSize; private final long photoSize;
/** /**
* Constructor. * Constructor.
* *
* @param contentProviderUtils the content provider utils * @param contentProviderUtils the content provider utils
* @param fileTrackExporter the file track exporter * @param fileTrackExporter the file track exporter
* @param tracks the tracks to export * @param tracks the tracks to export
* @param context the context * @param context the context
*/ */
public KmzTrackExporter(ContentProviderUtils contentProviderUtils, public KmzTrackExporter(ContentProviderUtils contentProviderUtils,
FileTrackExporter fileTrackExporter, Track[] tracks, Context context) { FileTrackExporter fileTrackExporter, Track[] tracks, Context context) {
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
this.fileTrackExporter = fileTrackExporter; this.fileTrackExporter = fileTrackExporter;
this.tracks = tracks; this.tracks = tracks;
this.photoSize = PreferencesUtils.getInt( this.photoSize = PreferencesUtils.getInt(
context, R.string.photo_size_key, PreferencesUtils.PHOTO_SIZE_DEFAULT); context, R.string.photo_size_key, PreferencesUtils.PHOTO_SIZE_DEFAULT);
} }
@Override @Override
public boolean writeTrack(OutputStream outputStream) { public boolean writeTrack(OutputStream outputStream) {
ZipOutputStream zipOutputStream = null; ZipOutputStream zipOutputStream = null;
try {
zipOutputStream = new ZipOutputStream(outputStream);
// Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
zipOutputStream.putNextEntry(zipEntry);
boolean success = fileTrackExporter.writeTrack(zipOutputStream);
zipOutputStream.closeEntry();
if (!success) {
Log.e(TAG, "Unable to write kml in kmz");
return false;
}
// Add photos
addImages(zipOutputStream);
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to write track", e);
return false;
} catch (IOException e) {
Log.e(TAG, "Unable to write track", e);
return false;
} finally {
if (zipOutputStream != null) {
try { try {
zipOutputStream.close(); zipOutputStream = new ZipOutputStream(outputStream);
// Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
zipOutputStream.putNextEntry(zipEntry);
boolean success = fileTrackExporter.writeTrack(zipOutputStream);
zipOutputStream.closeEntry();
if (!success) {
Log.e(TAG, "Unable to write kml in kmz");
return false;
}
// Add photos
addImages(zipOutputStream);
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to write track", e);
return false;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Unable to close zip input stream", e); Log.e(TAG, "Unable to write track", e);
} return false;
} } finally {
} if (zipOutputStream != null) {
} try {
zipOutputStream.close();
private void addImages(ZipOutputStream zipOutputStream) throws InterruptedException, IOException { } catch (IOException e) {
for (Track track : tracks) { Log.e(TAG, "Unable to close zip input stream", e);
try (Cursor cursor = contentProviderUtils.getWaypointCursor(track.getId(), -1L, -1)) { }
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as
* the first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
} }
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
if (waypoint.hasPhoto()) {
addImage(zipOutputStream, waypoint.getPhotoUrl());
}
}
} }
}
}
}
private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
Uri uri = Uri.parse(photoUrl);
File file = new File(uri.getPath());
if (!file.exists()) {
Log.e(TAG, "file not found " + photoUrl);
return;
} }
ZipEntry zipEntry = new ZipEntry( private void addImages(ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment()); for (Track track : tracks) {
zipOutputStream.putNextEntry(zipEntry); try (Cursor cursor = contentProviderUtils.getWaypointCursor(track.getId(), -1L, -1)) {
if (cursor != null && cursor.moveToFirst()) {
int sampleSize; /*
if (photoSize == -1) { * Yes, this will skip the first waypoint and that is intentional as
sampleSize = 1; * the first waypoint holds the stats for the track.
} else { */
long size = file.length(); while (cursor.moveToNext()) {
// Convert from kilobytes to bytes. if (Thread.interrupted()) {
long limit = photoSize * 1024; throw new InterruptedException();
sampleSize = size > limit ? (int) Math.ceil(size / limit) : 1; }
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
if (waypoint.hasPhoto()) {
addImage(zipOutputStream, waypoint.getPhotoUrl());
}
}
}
}
}
} }
if (sampleSize == 1) {
readFromFile(zipOutputStream, uri);
} else {
readFromScaledBitmap(zipOutputStream, uri, sampleSize);
}
zipOutputStream.closeEntry();
}
private void readFromScaledBitmap(ZipOutputStream zipOutputStream, Uri uri, int sampleSize) { private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options(); Uri uri = Uri.parse(photoUrl);
options.inJustDecodeBounds = false; File file = new File(uri.getPath());
options.inSampleSize = sampleSize; if (!file.exists()) {
Log.e(TAG, "file not found " + photoUrl);
return;
}
Bitmap bitmap = BitmapFactory.decodeFile(uri.getPath(), options); ZipEntry zipEntry = new ZipEntry(
if (bitmap == null) { KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment());
return; zipOutputStream.putNextEntry(zipEntry);
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, zipOutputStream);
bitmap.recycle();
}
private void readFromFile(ZipOutputStream zipOutputStream, Uri uri) throws IOException { int sampleSize;
try (FileInputStream fileInputStream = new FileInputStream(new File(uri.getPath()))) { if (photoSize == -1) {
byte[] buffer = new byte[BUFFER_SIZE]; sampleSize = 1;
int byteCount; } else {
while ((byteCount = fileInputStream.read(buffer)) != -1) { long size = file.length();
zipOutputStream.write(buffer, 0, byteCount); // Convert from kilobytes to bytes.
} long limit = photoSize * 1024;
sampleSize = size > limit ? (int) Math.ceil(size / limit) : 1;
}
if (sampleSize == 1) {
readFromFile(zipOutputStream, uri);
} else {
readFromScaledBitmap(zipOutputStream, uri, sampleSize);
}
zipOutputStream.closeEntry();
}
private void readFromScaledBitmap(ZipOutputStream zipOutputStream, Uri uri, int sampleSize) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
Bitmap bitmap = BitmapFactory.decodeFile(uri.getPath(), options);
if (bitmap == null) {
return;
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, zipOutputStream);
bitmap.recycle();
}
private void readFromFile(ZipOutputStream zipOutputStream, Uri uri) throws IOException {
try (FileInputStream fileInputStream = new FileInputStream(new File(uri.getPath()))) {
byte[] buffer = new byte[BUFFER_SIZE];
int byteCount;
while ((byteCount = fileInputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, byteCount);
}
}
} }
}
} }
@@ -16,12 +16,6 @@
package com.google.android.apps.mytracks.io.file.exporter; package com.google.android.apps.mytracks.io.file.exporter;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
@@ -31,6 +25,12 @@ import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import java.io.File; import java.io.File;
/** /**
@@ -41,187 +41,187 @@ import java.io.File;
*/ */
public class SaveActivity extends Activity { public class SaveActivity extends Activity {
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format"; public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
public static final String EXTRA_TRACK_IDS = "track_ids"; public static final String EXTRA_TRACK_IDS = "track_ids";
@Deprecated //TODO Seems to be a left over from Google Earth integration and can be removed. @Deprecated //TODO Seems to be a left over from Google Earth integration and can be removed.
public static final String EXTRA_PLAY_TRACK = "play_track"; public static final String EXTRA_PLAY_TRACK = "play_track";
private static final int DIALOG_PROGRESS_ID = 0; private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1; private static final int DIALOG_RESULT_ID = 1;
private TrackFileFormat trackFileFormat; private TrackFileFormat trackFileFormat;
private long[] trackIds; private long[] trackIds;
private boolean playTrack; private boolean playTrack;
private String directoryDisplayName; private String directoryDisplayName;
private SaveAsyncTask saveAsyncTask; private SaveAsyncTask saveAsyncTask;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
// the number of tracks successfully saved // the number of tracks successfully saved
private int successCount; private int successCount;
// the number of tracks to save // the number of tracks to save
private int totalCount; private int totalCount;
// the last successfully saved path // the last successfully saved path
private String savedPath; private String savedPath;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Intent intent = getIntent(); Intent intent = getIntent();
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT); trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS); trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false); playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false);
if (!FileUtils.isExternalStorageWriteable()) { if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish(); finish();
return; return;
} }
File directory = playTrack ? new File(getCacheDir(), FileUtils.PLAY_TRACKS_DIR) File directory = playTrack ? new File(getCacheDir(), FileUtils.PLAY_TRACKS_DIR)
: new File(FileUtils.getPath(trackFileFormat.getExtension())); : new File(FileUtils.getPath(trackFileFormat.getExtension()));
if (!FileUtils.ensureDirectoryExists(directory)) { if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish(); finish();
return; return;
} }
if (playTrack) { if (playTrack) {
for (File file : directory.listFiles()) { for (File file : directory.listFiles()) {
file.delete(); file.delete();
} }
} }
directoryDisplayName = playTrack ? directory.getName() directoryDisplayName = playTrack ? directory.getName()
: FileUtils.getPathDisplayName(trackFileFormat.getExtension()); : FileUtils.getPathDisplayName(trackFileFormat.getExtension());
Object retained = getLastNonConfigurationInstance(); Object retained = getLastNonConfigurationInstance();
if (retained instanceof SaveAsyncTask) { if (retained instanceof SaveAsyncTask) {
saveAsyncTask = (SaveAsyncTask) retained; saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this); saveAsyncTask.setActivity(this);
} else {
saveAsyncTask = new SaveAsyncTask(this, trackIds, trackFileFormat, playTrack, directory);
saveAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
saveAsyncTask.setActivity(null);
return saveAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
R.string.export_external_storage_progress_message,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
if (successCount == totalCount) {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_external_storage_success, totalTracks, directoryDisplayName);
} else { } else {
iconId = android.R.drawable.ic_dialog_alert; saveAsyncTask = new SaveAsyncTask(this, trackIds, trackFileFormat, playTrack, directory);
titleId = R.string.generic_error_title; saveAsyncTask.execute();
message = getString(R.string.export_external_storage_error, successCount, totalTracks,
directoryDisplayName);
} }
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true) }
.setIcon(iconId).setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override
@Override public Object onRetainNonConfigurationInstance() {
public void onCancel(DialogInterface dialog) { saveAsyncTask.setActivity(null);
dialog.dismiss(); return saveAsyncTask;
finish(); }
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { @Override
@Override protected Dialog onCreateDialog(int id) {
public void onClick(DialogInterface dialog, int arg1) { switch (id) {
dialog.dismiss(); case DIALOG_PROGRESS_ID:
finish(); progressDialog = DialogUtils.createHorizontalProgressDialog(this,
} R.string.export_external_storage_progress_message,
}).setTitle(titleId); new DialogInterface.OnCancelListener() {
if (!playTrack && trackIds.length == 1 && trackIds[0] != -1L && successCount == totalCount @Override
&& savedPath != null) { public void onCancel(DialogInterface dialog) {
builder.setNegativeButton( saveAsyncTask.cancel(true);
R.string.share_track_share_file, new DialogInterface.OnClickListener() { dialog.dismiss();
@Override }
public void onClick(DialogInterface dialog, int which) { }, directoryDisplayName);
Intent intent = IntentUtils.newShareFileIntent( return progressDialog;
SaveActivity.this, trackIds[0], savedPath, trackFileFormat); case DIALOG_RESULT_ID:
startActivity( int iconId;
Intent.createChooser(intent, getString(R.string.share_track_picker_title))); int titleId;
finish(); String message;
String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
if (successCount == totalCount) {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_external_storage_success, totalTracks, directoryDisplayName);
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(R.string.export_external_storage_error, successCount, totalTracks,
directoryDisplayName);
} }
}); AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
}).setTitle(titleId);
if (!playTrack && trackIds.length == 1 && trackIds[0] != -1L && successCount == totalCount
&& savedPath != null) {
builder.setNegativeButton(
R.string.share_track_share_file, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = IntentUtils.newShareFileIntent(
SaveActivity.this, trackIds[0], savedPath, trackFileFormat);
startActivity(
Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
finish();
}
});
}
final Dialog dialog = builder.create();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(SaveActivity.this, dialog);
}
});
return dialog;
default:
return null;
} }
final Dialog dialog = builder.create();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(SaveActivity.this, dialog);
}
});
return dialog;
default:
return null;
} }
}
/** /**
* Invokes when the associated AsyncTask completes. * Invokes when the associated AsyncTask completes.
* *
* @param aSuccessCount the number of tracks successfully saved * @param aSuccessCount the number of tracks successfully saved
* @param aTotalCount the number of tracks to save * @param aTotalCount the number of tracks to save
* @param aSavedPath the last successfully saved path * @param aSavedPath the last successfully saved path
*/ */
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) { public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) {
successCount = aSuccessCount; successCount = aSuccessCount;
totalCount = aTotalCount; totalCount = aTotalCount;
savedPath = aSavedPath; savedPath = aSavedPath;
removeDialog(DIALOG_PROGRESS_ID); removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID); showDialog(DIALOG_RESULT_ID);
} }
/** /**
* Shows the progress dialog. * Shows the progress dialog.
*/ */
public void showProgressDialog() { public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID); showDialog(DIALOG_PROGRESS_ID);
} }
/** /**
* Sets the progress dialog value. * Sets the progress dialog value.
* *
* @param number the number of points saved * @param number the number of points saved
* @param max the maximum number of points * @param max the maximum number of points
*/ */
public void setProgressDialogValue(int number, int max) { public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) { if (progressDialog != null) {
progressDialog.setIndeterminate(false); progressDialog.setIndeterminate(false);
progressDialog.setMax(max); progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max)); progressDialog.setProgress(Math.min(number, max));
}
} }
}
} }
@@ -43,215 +43,213 @@ import java.io.IOException;
*/ */
public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> { public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = SaveAsyncTask.class.getSimpleName(); private static final String TAG = SaveAsyncTask.class.getSimpleName();
private final long[] trackIds;
private final TrackFileFormat trackFileFormat;
@Deprecated //TODO Seems to be a left over from Google Earth integration and can be removed.
private final boolean playTrack;
private final File directory;
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private SaveActivity saveActivity;
private WakeLock wakeLock;
private SaveActivity saveActivity; // true if the AsyncTask has completed
private final long[] trackIds; private boolean completed;
private final TrackFileFormat trackFileFormat;
@Deprecated //TODO Seems to be a left over from Google Earth integration and can be removed.
private final boolean playTrack;
private final File directory;
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private WakeLock wakeLock; // the number of tracks successfully saved
private int successCount;
// true if the AsyncTask has completed // the number of tracks to save
private boolean completed; private int totalCount;
// the number of tracks successfully saved // the last successfully saved path
private int successCount; private String savedPath;
// the number of tracks to save /**
private int totalCount; * Creates an AsyncTask.
*
* @param saveActivity the activity currently associated with this task
* @param trackIds the track ids to save. To save all, set to size 1 with
* trackIds[0] == -1L
* @param trackFileFormat the track file format
* @param playTrack true to play track
* @param directory the directory to write the file
*/
public SaveAsyncTask(SaveActivity saveActivity, long[] trackIds, TrackFileFormat trackFileFormat, boolean playTrack, File directory) {
this.saveActivity = saveActivity;
this.trackIds = trackIds;
this.trackFileFormat = trackFileFormat;
this.playTrack = playTrack;
this.directory = directory;
context = saveActivity.getApplicationContext();
contentProviderUtils = ContentProviderUtils.Factory.get(context);
// the last successfully saved path completed = false;
private String savedPath; successCount = 0;
totalCount = 0;
/** savedPath = null;
* Creates an AsyncTask.
*
* @param saveActivity the activity currently associated with this task
* @param trackIds the track ids to save. To save all, set to size 1 with
* trackIds[0] == -1L
* @param trackFileFormat the track file format
* @param playTrack true to play track
* @param directory the directory to write the file
*/
public SaveAsyncTask(SaveActivity saveActivity, long[] trackIds, TrackFileFormat trackFileFormat, boolean playTrack, File directory) {
this.saveActivity = saveActivity;
this.trackIds = trackIds;
this.trackFileFormat = trackFileFormat;
this.playTrack = playTrack;
this.directory = directory;
context = saveActivity.getApplicationContext();
contentProviderUtils = ContentProviderUtils.Factory.get(context);
completed = false;
successCount = 0;
totalCount = 0;
savedPath = null;
}
/**
* Sets the current activity associated with this AsyncTask.
*
* @param saveActivity the current activity, can be null
*/
public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity;
if (completed && saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
} }
}
@Override /**
protected void onPreExecute() { * Sets the current activity associated with this AsyncTask.
if (saveActivity != null) { *
saveActivity.showProgressDialog(); * @param saveActivity the current activity, can be null
} */
} public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity;
@Override if (completed && saveActivity != null) {
protected Boolean doInBackground(Void... params) { saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
boolean isRecording = PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(saveActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
// Get the wake lock if not recording or paused
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
}
if (trackIds.length == 1 && trackIds[0] == -1L) {
return saveAllTracks();
} else {
totalCount = 1;
Track[] tracks = new Track[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
tracks[i] = contentProviderUtils.getTrack(trackIds[i]);
if (tracks[i] == null) {
Log.d(TAG, "No track for " + trackIds[i]);
return false;
}
} }
if (saveTracks(tracks)) { }
successCount = 1;
return true; @Override
} else { protected void onPreExecute() {
return false; if (saveActivity != null) {
saveActivity.showProgressDialog();
} }
}
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (saveActivity != null) {
saveActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@Override
protected void onCancelled() {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, null);
}
}
/**
* Saves tracks to one file.
*
* @param tracks the tracks
*/
private Boolean saveTracks(Track[] tracks) {
if (tracks.length == 0) {
return false;
} }
Track track = tracks[0]; @Override
boolean useKmz = trackFileFormat == TrackFileFormat.KML && !playTrack; protected Boolean doInBackground(Void... params) {
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, tracks,
trackFileFormat.newTrackWriter(context, tracks.length > 1, playTrack),
new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
/*
* If only saving one track, update the progress dialog once every
* 500 points
*/
if (trackIds.length == 1 && trackIds[0] != -1L && number % 500 == 0) {
publishProgress(number, max);
}
}
});
TrackExporter trackExporter = useKmz ? new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, context) : fileTrackExporter;
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
File file = new File(directory, fileName);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
if (trackExporter.writeTrack(fileOutputStream)) {
savedPath = file.getAbsolutePath();
return true;
} else {
if (!file.delete()) {
Log.d(TAG, "Unable to delete file");
}
Log.e(TAG, "Unable to export track");
return false;
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
} finally {
if (fileOutputStream != null) {
try { try {
fileOutputStream.close(); Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
} catch (IOException e) { boolean isRecording = PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
Log.e(TAG, "Unable to close file output stream", e); boolean isPaused = PreferencesUtils.getBoolean(saveActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
// Get the wake lock if not recording or paused
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
}
if (trackIds.length == 1 && trackIds[0] == -1L) {
return saveAllTracks();
} else {
totalCount = 1;
Track[] tracks = new Track[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
tracks[i] = contentProviderUtils.getTrack(trackIds[i]);
if (tracks[i] == null) {
Log.d(TAG, "No track for " + trackIds[i]);
return false;
}
}
if (saveTracks(tracks)) {
successCount = 1;
return true;
} else {
return false;
}
}
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
} }
}
} }
}
/** @Override
* Saves all the tracks. protected void onProgressUpdate(Integer... values) {
*/ if (saveActivity != null) {
private Boolean saveAllTracks() { saveActivity.setProgressDialogValue(values[0], values[1]);
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) { }
if (cursor == null) { }
return false;
} @Override
totalCount = cursor.getCount(); protected void onPostExecute(Boolean result) {
for (int i = 0; i < totalCount; i++) { completed = true;
if (isCancelled()) { if (saveActivity != null) {
return false; saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
} }
cursor.moveToPosition(i); }
Track track = contentProviderUtils.createTrack(cursor);
if (track != null && saveTracks(new Track[]{track})) { @Override
successCount++; protected void onCancelled() {
} completed = true;
publishProgress(i + 1, totalCount); if (saveActivity != null) {
} saveActivity.onAsyncTaskCompleted(successCount, totalCount, null);
return true; }
} }
}
/**
* Saves tracks to one file.
*
* @param tracks the tracks
*/
private Boolean saveTracks(Track[] tracks) {
if (tracks.length == 0) {
return false;
}
Track track = tracks[0];
boolean useKmz = trackFileFormat == TrackFileFormat.KML && !playTrack;
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, tracks,
trackFileFormat.newTrackWriter(context, tracks.length > 1, playTrack),
new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
/*
* If only saving one track, update the progress dialog once every
* 500 points
*/
if (trackIds.length == 1 && trackIds[0] != -1L && number % 500 == 0) {
publishProgress(number, max);
}
}
});
TrackExporter trackExporter = useKmz ? new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, context) : fileTrackExporter;
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
File file = new File(directory, fileName);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
if (trackExporter.writeTrack(fileOutputStream)) {
savedPath = file.getAbsolutePath();
return true;
} else {
if (!file.delete()) {
Log.d(TAG, "Unable to delete file");
}
Log.e(TAG, "Unable to export track");
return false;
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file output stream", e);
}
}
}
}
/**
* Saves all the tracks.
*/
private Boolean saveAllTracks() {
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
if (cursor == null) {
return false;
}
totalCount = cursor.getCount();
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
return false;
}
cursor.moveToPosition(i);
Track track = contentProviderUtils.createTrack(cursor);
if (track != null && saveTracks(new Track[]{track})) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
return true;
}
}
} }
@@ -25,10 +25,10 @@ import java.io.OutputStream;
*/ */
public interface TrackExporter { public interface TrackExporter {
/** /**
* Write track to an output stream * Write track to an output stream
* *
* @param outputStream the output stream * @param outputStream the output stream
*/ */
boolean writeTrack(OutputStream outputStream); boolean writeTrack(OutputStream outputStream);
} }
@@ -22,12 +22,12 @@ package com.google.android.apps.mytracks.io.file.exporter;
*/ */
public interface TrackExporterListener { public interface TrackExporterListener {
/** /**
* Called to update progress. * Called to update progress.
* *
* @param number the number of locations written * @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of * @param max the maximum number of locations in a track, for calculation of
* completion percentage * completion percentage
*/ */
void onProgressUpdate(int number, int max); void onProgressUpdate(int number, int max);
} }
@@ -15,11 +15,11 @@
*/ */
package com.google.android.apps.mytracks.io.file.exporter; package com.google.android.apps.mytracks.io.file.exporter;
import android.location.Location;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location;
import java.io.OutputStream; import java.io.OutputStream;
/** /**
@@ -51,94 +51,94 @@ import java.io.OutputStream;
*/ */
public interface TrackWriter { public interface TrackWriter {
/** /**
* Gets the file extension (e.g, gpx, kml, ...). * Gets the file extension (e.g, gpx, kml, ...).
*/ */
String getExtension(); String getExtension();
/** /**
* Prepares the output stream. * Prepares the output stream.
* *
* @param outputStream the output stream * @param outputStream the output stream
*/ */
void prepare(OutputStream outputStream); void prepare(OutputStream outputStream);
/** /**
* Closes the output stream. * Closes the output stream.
*/ */
void close(); void close();
/** /**
* Writes the header * Writes the header
* *
* @param tracks the tracks * @param tracks the tracks
*/ */
void writeHeader(Track[] tracks); void writeHeader(Track[] tracks);
/** /**
* Writes the footer. * Writes the footer.
*/ */
void writeFooter(); void writeFooter();
/** /**
* Writes the beginning of the waypoints. * Writes the beginning of the waypoints.
* *
* @param track the track * @param track the track
*/ */
void writeBeginWaypoints(Track track); void writeBeginWaypoints(Track track);
/** /**
* Writes the end of the waypoints. * Writes the end of the waypoints.
*/ */
void writeEndWaypoints(); void writeEndWaypoints();
/** /**
* Writes a waypoint. * Writes a waypoint.
* *
* @param waypoint the waypoint * @param waypoint the waypoint
*/ */
void writeWaypoint(Waypoint waypoint); void writeWaypoint(Waypoint waypoint);
/** /**
* Writes the beginning of the tracks. * Writes the beginning of the tracks.
*/ */
void writeBeginTracks(); void writeBeginTracks();
/** /**
* Writes the end of the tracks, * Writes the end of the tracks,
*/ */
void writeEndTracks(); void writeEndTracks();
/** /**
* Writes the beginning of a track. * Writes the beginning of a track.
* *
* @param track the track * @param track the track
* @param startLocation the start location * @param startLocation the start location
*/ */
void writeBeginTrack(Track track, Location startLocation); void writeBeginTrack(Track track, Location startLocation);
/** /**
* Writes the end of a track. * Writes the end of a track.
* *
* @param track the track * @param track the track
* @param endLocation the end location * @param endLocation the end location
*/ */
void writeEndTrack(Track track, Location endLocation); void writeEndTrack(Track track, Location endLocation);
/** /**
* Writes open segment. * Writes open segment.
*/ */
void writeOpenSegment(); void writeOpenSegment();
/** /**
* Writes close segment. * Writes close segment.
*/ */
void writeCloseSegment(); void writeCloseSegment();
/** /**
* Writes a location. * Writes a location.
* *
* @param location the location * @param location the location
*/ */
void writeLocation(Location location); void writeLocation(Location location);
} }
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.location.Location; import android.location.Location;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
@@ -33,158 +34,158 @@ import org.xml.sax.SAXException;
*/ */
public class GpxFileTrackImporter extends AbstractFileTrackImporter { public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_DESCRIPTION = "desc"; private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt"; private static final String TAG_COMMENT = "cmt";
private static final String TAG_ELEVATION = "ele"; private static final String TAG_ELEVATION = "ele";
private static final String TAG_GPX = "gpx"; private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name"; private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time"; private static final String TAG_TIME = "time";
private static final String TAG_TRACK = "trk"; private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt"; private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg"; private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type"; private static final String TAG_TYPE = "type";
private static final String TAG_WAYPOINT = "wpt"; private static final String TAG_WAYPOINT = "wpt";
private static final String ATTRIBUTE_LAT = "lat"; private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon"; private static final String ATTRIBUTE_LON = "lon";
/** /**
* Constructor. * Constructor.
* *
* @param context the context * @param context the context
*/ */
public GpxFileTrackImporter(Context context) { public GpxFileTrackImporter(Context context) {
this(context, ContentProviderUtils.Factory.get(context)); this(context, ContentProviderUtils.Factory.get(context));
}
@VisibleForTesting
GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
super(context, -1L, contentProviderUtils);
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
switch (tag) {
case TAG_WAYPOINT:
onWaypointStart(attributes);
break;
case TAG_TRACK:
onTrackStart();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentStart();
break;
case TAG_TRACK_POINT:
onTrackPointStart(attributes);
break;
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) {
case TAG_GPX:
onFileEnd();
break;
case TAG_WAYPOINT:
onWaypointEnd();
break;
case TAG_TRACK:
onTrackEnd();
break;
case TAG_TRACK_POINT:
onTrackPointEnd();
break;
case TAG_NAME:
if (content != null) {
name = content.trim();
}
break;
case TAG_DESCRIPTION:
if (content != null) {
description = content.trim();
}
break;
case TAG_TYPE:
if (content != null) {
category = content.trim();
}
break;
case TAG_TIME:
if (content != null) {
time = content.trim();
}
break;
case TAG_ELEVATION:
if (content != null) {
altitude = content.trim();
}
break;
case TAG_COMMENT:
if (content != null) {
waypointType = content.trim();
}
break;
}
// Reset element content
content = null;
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
}
/**
* On track point end.
*/
private void onTrackPointEnd() throws SAXException {
Location location = getTrackPoint();
if (location == null) {
return;
} }
insertTrackPoint(location);
}
/** @VisibleForTesting
* On waypoint start. GpxFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils) {
* super(context, -1L, contentProviderUtils);
* @param attributes the attributes }
*/
private void onWaypointStart(Attributes attributes) {
name = null;
description = null;
category = null;
photoUrl = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
waypointType = null;
}
/** @Override
* On waypoint end. public void startElement(String uri, String localName, String tag, Attributes attributes)
*/ throws SAXException {
private void onWaypointEnd() throws SAXException { switch (tag) {
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS case TAG_WAYPOINT:
: WaypointType.WAYPOINT); onWaypointStart(attributes);
} break;
case TAG_TRACK:
onTrackStart();
break;
case TAG_TRACK_SEGMENT:
onTrackSegmentStart();
break;
case TAG_TRACK_POINT:
onTrackPointStart(attributes);
break;
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) {
case TAG_GPX:
onFileEnd();
break;
case TAG_WAYPOINT:
onWaypointEnd();
break;
case TAG_TRACK:
onTrackEnd();
break;
case TAG_TRACK_POINT:
onTrackPointEnd();
break;
case TAG_NAME:
if (content != null) {
name = content.trim();
}
break;
case TAG_DESCRIPTION:
if (content != null) {
description = content.trim();
}
break;
case TAG_TYPE:
if (content != null) {
category = content.trim();
}
break;
case TAG_TIME:
if (content != null) {
time = content.trim();
}
break;
case TAG_ELEVATION:
if (content != null) {
altitude = content.trim();
}
break;
case TAG_COMMENT:
if (content != null) {
waypointType = content.trim();
}
break;
}
// Reset element content
content = null;
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
}
/**
* On track point end.
*/
private void onTrackPointEnd() throws SAXException {
Location location = getTrackPoint();
if (location == null) {
return;
}
insertTrackPoint(location);
}
/**
* On waypoint start.
*
* @param attributes the attributes
*/
private void onWaypointStart(Attributes attributes) {
name = null;
description = null;
category = null;
photoUrl = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
waypointType = null;
}
/**
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS
: WaypointType.WAYPOINT);
}
} }
@@ -46,194 +46,194 @@ import java.io.File;
*/ */
public class ImportActivity extends Activity { public class ImportActivity extends Activity {
public static final String EXTRA_IMPORT_ALL = "import_all"; public static final String EXTRA_IMPORT_ALL = "import_all";
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format"; public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
private static final String TAG = ImportActivity.class.getSimpleName(); private static final String TAG = ImportActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0; private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1; private static final int DIALOG_RESULT_ID = 1;
private ImportAsyncTask importAsyncTask; private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
private boolean importAll; private boolean importAll;
// the path on the external storage to import // the path on the external storage to import
private String directoryDisplayName; private String directoryDisplayName;
// the number of files successfully imported // the number of files successfully imported
private int successCount; private int successCount;
// the number of files to import // the number of files to import
private int totalCount; private int totalCount;
// the last successfully imported track id // the last successfully imported track id
private long trackId; private long trackId;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Intent intent = getIntent(); Intent intent = getIntent();
importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false); importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false);
TrackFileFormat trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT); TrackFileFormat trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
if (trackFileFormat == null) { if (trackFileFormat == null) {
trackFileFormat = TrackFileFormat.GPX; trackFileFormat = TrackFileFormat.GPX;
}
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
String directoryPath;
if (importAll) {
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(
this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
} else {
String action = intent.getAction();
if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
Log.d(TAG, "Invalid action: " + intent);
finish();
return;
}
Uri data = intent.getData();
if ("file".equals(data.getScheme())) {
Log.d(TAG, "Invalid data: " + intent);
finish();
return;
}
directoryDisplayName = data.getPath();
directoryPath = data.getPath();
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ImportAsyncTask) {
importAsyncTask = (ImportAsyncTask) retained;
importAsyncTask.setActivity(this);
} else {
importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, directoryPath);
importAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
importAsyncTask.setActivity(null);
return importAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalCount, totalCount);
if (successCount == totalCount) {
if (totalCount == 0) {
iconId = android.R.drawable.ic_dialog_info;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, successCount, totalFiles, directoryDisplayName);
} }
final Dialog dialog = new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() { if (!FileUtils.isExternalStorageAvailable()) {
@Override Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
public void onCancel(DialogInterface dialogInterface) { finish();
dialogInterface.dismiss(); return;
}
String directoryPath;
if (importAll) {
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(
this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish(); finish();
} return;
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { }
@Override } else {
public void onClick(DialogInterface dialogInterface, int which) { String action = intent.getAction();
if (successCount == totalCount && !importAll && trackId != -1L) { if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
Intent intent = IntentUtils.newIntent( Log.d(TAG, "Invalid action: " + intent);
ImportActivity.this, TrackDetailActivity.class) finish();
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); return;
TaskStackBuilder.create(ImportActivity.this) }
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
.startActivities(); Uri data = intent.getData();
if ("file".equals(data.getScheme())) {
Log.d(TAG, "Invalid data: " + intent);
finish();
return;
}
directoryDisplayName = data.getPath();
directoryPath = data.getPath();
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ImportAsyncTask) {
importAsyncTask = (ImportAsyncTask) retained;
importAsyncTask.setActivity(this);
} else {
importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, directoryPath);
importAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
importAsyncTask.setActivity(null);
return importAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalCount, totalCount);
if (successCount == totalCount) {
if (totalCount == 0) {
iconId = android.R.drawable.ic_dialog_info;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, successCount, totalFiles, directoryDisplayName);
} }
dialogInterface.dismiss(); final Dialog dialog = new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
finish(); .setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
} @Override
}).setTitle(titleId).create(); public void onCancel(DialogInterface dialogInterface) {
dialog.setOnShowListener(new DialogInterface.OnShowListener() { dialogInterface.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
if (successCount == totalCount && !importAll && trackId != -1L) {
Intent intent = IntentUtils.newIntent(
ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
TaskStackBuilder.create(ImportActivity.this)
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
.startActivities();
}
dialogInterface.dismiss();
finish();
}
}).setTitle(titleId).create();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override @Override
public void onShow(DialogInterface dialogInterface) { public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(ImportActivity.this, dialog); DialogUtils.setDialogTitleDivider(ImportActivity.this, dialog);
} }
}); });
return dialog; return dialog;
default: default:
return null; return null;
}
} }
}
/** /**
* Invokes when the associated AsyncTask completes. * Invokes when the associated AsyncTask completes.
* *
* @param aSuccessCount the number of files successfully imported * @param aSuccessCount the number of files successfully imported
* @param aTotalCount the number of files to import * @param aTotalCount the number of files to import
* @param aTrackId the last successfully imported track id * @param aTrackId the last successfully imported track id
*/ */
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) { public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) {
successCount = aSuccessCount; successCount = aSuccessCount;
totalCount = aTotalCount; totalCount = aTotalCount;
trackId = aTrackId; trackId = aTrackId;
removeDialog(DIALOG_PROGRESS_ID); removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID); showDialog(DIALOG_RESULT_ID);
} }
/** /**
* Shows the progress dialog. * Shows the progress dialog.
*/ */
public void showProgressDialog() { public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID); showDialog(DIALOG_PROGRESS_ID);
} }
/** /**
* Sets the progress dialog value. * Sets the progress dialog value.
* *
* @param number the number of files imported * @param number the number of files imported
* @param max the maximum number of files * @param max the maximum number of files
*/ */
public void setProgressDialogValue(int number, int max) { public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) { if (progressDialog != null) {
progressDialog.setIndeterminate(false); progressDialog.setIndeterminate(false);
progressDialog.setMax(max); progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max)); progressDialog.setProgress(Math.min(number, max));
}
} }
}
} }
@@ -16,6 +16,12 @@
package com.google.android.apps.mytracks.io.file.importer; package com.google.android.apps.mytracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.file.TrackFileFormat; import com.google.android.apps.mytracks.io.file.TrackFileFormat;
@@ -25,12 +31,6 @@ import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -45,197 +45,196 @@ import java.util.List;
*/ */
public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> { public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName(); private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final boolean importAll;
private final TrackFileFormat trackFileFormat;
private final String path;
private final Context context;
private ImportActivity importActivity;
private WakeLock wakeLock;
private ImportActivity importActivity; // true if the AsyncTask has completed
private final boolean importAll; private boolean completed;
private final TrackFileFormat trackFileFormat;
private final String path;
private final Context context;
private WakeLock wakeLock;
// true if the AsyncTask has completed // the number of files successfully imported
private boolean completed; private int successCount;
// the number of files successfully imported // the number of files to import
private int successCount; private int totalCount;
// the number of files to import // the last successfully imported track id
private int totalCount; private long trackId;
// the last successfully imported track id /**
private long trackId; * Creates an AsyncTask.
*
* @param importActivity the activity currently associated with this AsyncTask
* @param importAll true to import all GPX files
* @param trackFileFormat the track file format
* @param path path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, boolean importAll,
TrackFileFormat trackFileFormat, String path) {
this.importActivity = importActivity;
this.importAll = importAll;
this.trackFileFormat = trackFileFormat;
this.path = path;
context = importActivity.getApplicationContext();
/** completed = false;
* Creates an AsyncTask. successCount = 0;
* totalCount = 0;
* @param importActivity the activity currently associated with this AsyncTask trackId = -1L;
* @param importAll true to import all GPX files
* @param trackFileFormat the track file format
* @param path path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, boolean importAll,
TrackFileFormat trackFileFormat, String path) {
this.importActivity = importActivity;
this.importAll = importAll;
this.trackFileFormat = trackFileFormat;
this.path = path;
context = importActivity.getApplicationContext();
completed = false;
successCount = 0;
totalCount = 0;
trackId = -1L;
}
/**
* Sets the current {@link ImportActivity} associated with this AyncTask.
*
* @param importActivity the current {@link ImportActivity}, can be null
*/
public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
if (completed && importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
} }
}
@Override /**
protected void onPreExecute() { * Sets the current {@link ImportActivity} associated with this AyncTask.
if (importActivity != null) { *
importActivity.showProgressDialog(); * @param importActivity the current {@link ImportActivity}, can be null
} */
} public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
@Override if (completed && importActivity != null) {
protected Boolean doInBackground(Void... params) { importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
// Get the wake lock if not recording or paused
boolean isRecording =
PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(importActivity,
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<File> files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
return true;
}
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
} }
if (importFile(files.get(i))) { }
successCount++;
@Override
protected void onPreExecute() {
if (importActivity != null) {
importActivity.showProgressDialog();
} }
publishProgress(i + 1, totalCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
} }
}
@Override @Override
protected void onProgressUpdate(Integer... values) { protected Boolean doInBackground(Void... params) {
if (importActivity != null) {
importActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@Override
protected void onCancelled() {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
FileInputStream fileInputStream = null;
try {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.KML) {
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
} else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
}
} else {
trackImporter = new GpxFileTrackImporter(context);
}
fileInputStream = new FileInputStream(file);
trackId = trackImporter.importFile(fileInputStream);
return trackId != -1L;
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to import file", e);
return false;
} finally {
if (fileInputStream != null) {
try { try {
fileInputStream.close(); Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
} catch (IOException e) { // Get the wake lock if not recording or paused
Log.e(TAG, "Unable to close file input stream", e); boolean isRecording =
} PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
} != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
} boolean isPaused = PreferencesUtils.getBoolean(importActivity,
} R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!isRecording || isPaused) {
/** wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
* Gets a list of files. If importAll is true, returns a list of the files }
* under the path directory. If importAll is false, returns a list containing
* just the path file. List<File> files = getFiles();
*/ totalCount = files.size();
private List<File> getFiles() { if (totalCount == 0) {
List<File> files = new ArrayList<>(); return true;
File file = new File(path); }
if (importAll) {
File[] candidates = file.listFiles(); for (int i = 0; i < totalCount; i++) {
if (candidates != null) { if (isCancelled()) {
for (File candidate : candidates) { // If cancelled, return true to show the number of files imported
if (!FileUtils.isDirectory(candidate)) { return true;
String extension = FileUtils.getExtension(candidate.getName()); }
if (trackFileFormat == TrackFileFormat.KML && ( if (importFile(files.get(i))) {
TrackFileFormat.KML.getExtension().equals(extension) successCount++;
|| KmzTrackExporter.KMZ_EXTENSION.equals(extension))) { }
files.add(candidate); publishProgress(i + 1, totalCount);
} else if (trackFileFormat == TrackFileFormat.GPX }
&& TrackFileFormat.GPX.getExtension().equals(extension)) { return true;
files.add(candidate); } finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
} }
}
} }
}
} else {
files.add(file);
} }
return files;
} @Override
protected void onProgressUpdate(Integer... values) {
if (importActivity != null) {
importActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@Override
protected void onCancelled() {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
FileInputStream fileInputStream = null;
try {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.KML) {
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
} else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
}
} else {
trackImporter = new GpxFileTrackImporter(context);
}
fileInputStream = new FileInputStream(file);
trackId = trackImporter.importFile(fileInputStream);
return trackId != -1L;
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to import file", e);
return false;
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file input stream", e);
}
}
}
}
/**
* Gets a list of files. If importAll is true, returns a list of the files
* under the path directory. If importAll is false, returns a list containing
* just the path file.
*/
private List<File> getFiles() {
List<File> files = new ArrayList<>();
File file = new File(path);
if (importAll) {
File[] candidates = file.listFiles();
if (candidates != null) {
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat == TrackFileFormat.KML && (
TrackFileFormat.KML.getExtension().equals(extension)
|| KmzTrackExporter.KMZ_EXTENSION.equals(extension))) {
files.add(candidate);
} else if (trackFileFormat == TrackFileFormat.GPX
&& TrackFileFormat.GPX.getExtension().equals(extension)) {
files.add(candidate);
}
}
}
}
} else {
files.add(file);
}
return files;
}
} }
@@ -22,8 +22,8 @@ import android.net.Uri;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType; import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.content.sensor.SensorDataSet; import com.google.android.apps.mytracks.content.sensor.SensorDataSet;
@@ -39,273 +39,273 @@ import java.util.ArrayList;
*/ */
public class KmlFileTrackImporter extends AbstractFileTrackImporter { public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String CADENCE = "cadence"; private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate"; private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power"; private static final String POWER = "power";
private static final String STATISTICS_STYLE = "#statistics"; private static final String STATISTICS_STYLE = "#statistics";
private static final String WAYPOINT_STYLE = "#waypoint"; private static final String WAYPOINT_STYLE = "#waypoint";
private static final String TAG_COORDINATES = "coordinates"; private static final String TAG_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description"; private static final String TAG_DESCRIPTION = "description";
private static final String TAG_GX_COORD = "gx:coord"; private static final String TAG_GX_COORD = "gx:coord";
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack"; private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData"; private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
private static final String TAG_GX_TRACK = "gx:Track"; private static final String TAG_GX_TRACK = "gx:Track";
private static final String TAG_GX_VALUE = "gx:value"; private static final String TAG_GX_VALUE = "gx:value";
private static final String TAG_HREF = "href"; private static final String TAG_HREF = "href";
private static final String TAG_KML = "kml"; private static final String TAG_KML = "kml";
private static final String TAG_NAME = "name"; private static final String TAG_NAME = "name";
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay"; private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark"; private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl"; private static final String TAG_STYLE_URL = "styleUrl";
private static final String TAG_VALUE = "value"; private static final String TAG_VALUE = "value";
private static final String TAG_WHEN = "when"; private static final String TAG_WHEN = "when";
private static final String ATTRIBUTE_NAME = "name"; private static final String ATTRIBUTE_NAME = "name";
private boolean trackStarted = false; private boolean trackStarted = false;
private String sensorName; private String sensorName;
private ArrayList<Location> locationList; private ArrayList<Location> locationList;
private ArrayList<Integer> cadenceList; private ArrayList<Integer> cadenceList;
private ArrayList<Integer> heartRateList; private ArrayList<Integer> heartRateList;
private ArrayList<Integer> powerList; private ArrayList<Integer> powerList;
/** /**
* Constructor. * Constructor.
* *
* @param context the context * @param context the context
* @param importTrackId track id to import to. -1L to import to a new track. * @param importTrackId track id to import to. -1L to import to a new track.
*/ */
public KmlFileTrackImporter(Context context, long importTrackId) { public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, ContentProviderUtils.Factory.get(context)); this(context, importTrackId, ContentProviderUtils.Factory.get(context));
} }
@VisibleForTesting @VisibleForTesting
KmlFileTrackImporter( KmlFileTrackImporter(
Context context, long importTrackId, ContentProviderUtils contentProviderUtils) { Context context, long importTrackId, ContentProviderUtils contentProviderUtils) {
super(context, importTrackId, contentProviderUtils); super(context, importTrackId, contentProviderUtils);
} }
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException { throws SAXException {
switch (tag) { switch (tag) {
case TAG_PLACEMARK: case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY: case TAG_PHOTO_OVERLAY:
/* /*
* Note that a track is contained in a Placemark, calling onWaypointStart * Note that a track is contained in a Placemark, calling onWaypointStart
* will clear various track variables like name, category, and * will clear various track variables like name, category, and
* description. * description.
*/ */
onWaypointStart(); onWaypointStart();
break; break;
case TAG_GX_MULTI_TRACK: case TAG_GX_MULTI_TRACK:
trackStarted = true; trackStarted = true;
onTrackStart(); onTrackStart();
break; break;
case TAG_GX_TRACK: case TAG_GX_TRACK:
if (!trackStarted) { if (!trackStarted) {
throw new SAXException("No " + TAG_GX_MULTI_TRACK); throw new SAXException("No " + TAG_GX_MULTI_TRACK);
}
onTrackSegmentStart();
break;
case TAG_GX_SIMPLE_ARRAY_DATA:
onSensorDataStart(attributes);
break;
} }
onTrackSegmentStart();
break;
case TAG_GX_SIMPLE_ARRAY_DATA:
onSensorDataStart(attributes);
break;
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
if (tag.equals(TAG_KML)) {
onFileEnd();
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
/*
* Note that a track is contained in a Placemark, calling onWaypointend is
* save since waypointType is not set for a track.
*/
onWaypointEnd();
} else if (localName.equals(TAG_COORDINATES)) {
onWaypointLocationEnd();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_GX_TRACK)) {
onTrackSegmentEnd();
} else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) {
onSensorValueEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (localName.equals(TAG_VALUE)) {
if (content != null) {
category = content.trim();
}
} else if (localName.equals(TAG_WHEN)) {
if (content != null) {
time = content.trim();
}
} else if (localName.equals(TAG_STYLE_URL)) {
if (content != null) {
waypointType = content.trim();
}
} else if (localName.equals(TAG_HREF)) {
if (content != null) {
photoUrl = content.trim();
}
} }
// Reset element content @Override
content = null; public void endElement(String uri, String localName, String tag) throws SAXException {
} if (tag.equals(TAG_KML)) {
onFileEnd();
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
/*
* Note that a track is contained in a Placemark, calling onWaypointend is
* save since waypointType is not set for a track.
*/
onWaypointEnd();
} else if (localName.equals(TAG_COORDINATES)) {
onWaypointLocationEnd();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_GX_TRACK)) {
onTrackSegmentEnd();
} else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) {
onSensorValueEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (localName.equals(TAG_VALUE)) {
if (content != null) {
category = content.trim();
}
} else if (localName.equals(TAG_WHEN)) {
if (content != null) {
time = content.trim();
}
} else if (localName.equals(TAG_STYLE_URL)) {
if (content != null) {
waypointType = content.trim();
}
} else if (localName.equals(TAG_HREF)) {
if (content != null) {
photoUrl = content.trim();
}
}
/** // Reset element content
* On waypoint start. content = null;
*/ }
private void onWaypointStart() {
// Reset all Placemark variables
name = null;
description = null;
category = null;
photoUrl = null;
latitude = null;
longitude = null;
altitude = null;
time = null;
waypointType = null;
}
/** /**
* On waypoint end. * On waypoint start.
*/ */
private void onWaypointEnd() throws SAXException { private void onWaypointStart() {
// Add a waypoint if the waypointType matches // Reset all Placemark variables
WaypointType type = null; name = null;
if (WAYPOINT_STYLE.equals(waypointType)) { description = null;
type = WaypointType.WAYPOINT; category = null;
} else if (STATISTICS_STYLE.equals(waypointType)) { photoUrl = null;
type = WaypointType.STATISTICS; latitude = null;
longitude = null;
altitude = null;
time = null;
waypointType = null;
} }
if (type == null) {
return;
}
if (photoUrl != null) {
Uri uri = Uri.parse(photoUrl);
photoUrl = getPhotoUrl(uri.getLastPathSegment());
}
addWaypoint(type);
}
/** /**
* On waypoint location end. * On waypoint end.
*/ */
private void onWaypointLocationEnd() { private void onWaypointEnd() throws SAXException {
if (content != null) { // Add a waypoint if the waypointType matches
String[] parts = content.trim().split(","); WaypointType type = null;
if (parts.length != 2 && parts.length != 3) { if (WAYPOINT_STYLE.equals(waypointType)) {
return; type = WaypointType.WAYPOINT;
} } else if (STATISTICS_STYLE.equals(waypointType)) {
longitude = parts[0]; type = WaypointType.STATISTICS;
latitude = parts[1]; }
altitude = parts.length == 3 ? parts[2] : null; if (type == null) {
return;
}
if (photoUrl != null) {
Uri uri = Uri.parse(photoUrl);
photoUrl = getPhotoUrl(uri.getLastPathSegment());
}
addWaypoint(type);
} }
}
@Override /**
protected void onTrackSegmentStart() { * On waypoint location end.
super.onTrackSegmentStart(); */
locationList = new ArrayList<>(); private void onWaypointLocationEnd() {
heartRateList = new ArrayList<>(); if (content != null) {
cadenceList = new ArrayList<>(); String[] parts = content.trim().split(",");
powerList = new ArrayList<>(); if (parts.length != 2 && parts.length != 3) {
} return;
}
/** longitude = parts[0];
* On track segment end. latitude = parts[1];
*/ altitude = parts.length == 3 ? parts[2] : null;
private void onTrackSegmentEnd() { }
// Close a track segment by inserting the segment locations
boolean hasHeartRate = heartRateList.size() == locationList.size();
boolean hasCadence = cadenceList.size() == locationList.size();
boolean hasPower = powerList.size() == locationList.size();
for (int i = 0; i < locationList.size(); i++) {
Location location = locationList.get(i);
if (!hasPower && !hasCadence && !hasHeartRate) {
insertTrackPoint(location);
} else {
float heartrate = hasHeartRate ? heartRateList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
float cadence = hasHeartRate ? cadenceList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
float power = hasHeartRate ? powerList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
SensorDataSetLocation sensorDataSetLocation = new SensorDataSetLocation(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
insertTrackPoint(sensorDataSetLocation);
}
} }
}
/** @Override
* On track point end. gx:coord end tag. protected void onTrackSegmentStart() {
*/ super.onTrackSegmentStart();
private void onTrackPointEnd() throws SAXException { locationList = new ArrayList<>();
// Add location to locationList heartRateList = new ArrayList<>();
if (content == null) { cadenceList = new ArrayList<>();
return; powerList = new ArrayList<>();
} }
String[] parts = content.trim().split(" ");
if (parts.length != 2 && parts.length != 3) {
return;
}
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
Location location = getTrackPoint(); /**
if (location == null) { * On track segment end.
return; */
} private void onTrackSegmentEnd() {
locationList.add(location); // Close a track segment by inserting the segment locations
time = null; boolean hasHeartRate = heartRateList.size() == locationList.size();
} boolean hasCadence = cadenceList.size() == locationList.size();
boolean hasPower = powerList.size() == locationList.size();
/** for (int i = 0; i < locationList.size(); i++) {
* On sensor data start. gx:SimpleArrayData start tag. Location location = locationList.get(i);
*/
private void onSensorDataStart(Attributes attributes) {
sensorName = attributes.getValue(ATTRIBUTE_NAME);
}
/** if (!hasPower && !hasCadence && !hasHeartRate) {
* On sensor value end. gx:value end tag. insertTrackPoint(location);
*/ } else {
private void onSensorValueEnd() throws SAXException { float heartrate = hasHeartRate ? heartRateList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
if (content == null) { float cadence = hasHeartRate ? cadenceList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
return; float power = hasHeartRate ? powerList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
SensorDataSetLocation sensorDataSetLocation = new SensorDataSetLocation(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
insertTrackPoint(sensorDataSetLocation);
}
}
} }
content = content.trim();
if (content.equals("")) { /**
return; * On track point end. gx:coord end tag.
*/
private void onTrackPointEnd() throws SAXException {
// Add location to locationList
if (content == null) {
return;
}
String[] parts = content.trim().split(" ");
if (parts.length != 2 && parts.length != 3) {
return;
}
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
Location location = getTrackPoint();
if (location == null) {
return;
}
locationList.add(location);
time = null;
} }
int value;
try { /**
value = Integer.parseInt(content); * On sensor data start. gx:SimpleArrayData start tag.
} catch (NumberFormatException e) { */
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); private void onSensorDataStart(Attributes attributes) {
sensorName = attributes.getValue(ATTRIBUTE_NAME);
} }
if (POWER.equals(sensorName)) {
powerList.add(value); /**
} else if (HEART_RATE.equals(sensorName)) { * On sensor value end. gx:value end tag.
heartRateList.add(value); */
} else if (CADENCE.equals(sensorName)) { private void onSensorValueEnd() throws SAXException {
cadenceList.add(value); if (content == null) {
return;
}
content = content.trim();
if (content.equals("")) {
return;
}
int value;
try {
value = Integer.parseInt(content);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
}
if (POWER.equals(sensorName)) {
powerList.add(value);
} else if (HEART_RATE.equals(sensorName)) {
heartRateList.add(value);
} else if (CADENCE.equals(sensorName)) {
cadenceList.add(value);
}
} }
}
} }
@@ -16,14 +16,14 @@
package com.google.android.apps.mytracks.io.file.importer; package com.google.android.apps.mytracks.io.file.importer;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.io.file.exporter.KmzTrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.io.file.exporter.KmzTrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@@ -40,159 +40,159 @@ import java.util.zip.ZipInputStream;
*/ */
public class KmzTrackImporter implements TrackImporter { public class KmzTrackImporter implements TrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName(); private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final int BUFFER_SIZE = 4096; private static final int BUFFER_SIZE = 4096;
private final Context context; private final Context context;
private final long importTrackId; private final long importTrackId;
/** /**
* Constructor. * Constructor.
* *
* @param context the context * @param context the context
* @param importTrackId track id to import to. This should not be -1L so that * @param importTrackId track id to import to. This should not be -1L so that
* images in the kmz file can be imported. * images in the kmz file can be imported.
*/ */
public KmzTrackImporter(Context context, long importTrackId) { public KmzTrackImporter(Context context, long importTrackId) {
this.context = context; this.context = context;
this.importTrackId = importTrackId; this.importTrackId = importTrackId;
} }
@Override @Override
public long importFile(InputStream inputStream) { public long importFile(InputStream inputStream) {
ZipInputStream zipInputStream = null; ZipInputStream zipInputStream = null;
long trackId = importTrackId; long trackId = importTrackId;
try { try {
ZipEntry zipEntry; ZipEntry zipEntry;
zipInputStream = new ZipInputStream(inputStream); zipInputStream = new ZipInputStream(inputStream);
while ((zipEntry = zipInputStream.getNextEntry()) != null) { while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted"); Log.d(TAG, "Thread interrupted");
cleanImport(trackId); cleanImport(trackId);
return -1L; return -1L;
} }
String fileName = zipEntry.getName(); String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) { if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) {
trackId = parseKml(zipInputStream); trackId = parseKml(zipInputStream);
if (trackId == -1L) { if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz"); Log.d(TAG, "Unable to parse kml in kmz");
cleanImport(trackId);
return -1L;
}
} else {
String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar;
if (fileName.startsWith(prefix)) {
readImageFile(zipInputStream, fileName.substring(prefix.length()));
}
}
zipInputStream.closeEntry();
}
return trackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport(trackId); cleanImport(trackId);
return -1L; return -1L;
} } finally {
} else { if (zipInputStream != null) {
String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar; try {
if (fileName.startsWith(prefix)) { zipInputStream.close();
readImageFile(zipInputStream, fileName.substring(prefix.length())); } catch (IOException e) {
} Log.e(TAG, "Unable to close zip input stream", e);
}
}
} }
zipInputStream.closeEntry(); }
}
return trackId; /**
} catch (IOException e) { * Cleans up import.
Log.e(TAG, "Unable to import file", e); *
cleanImport(trackId); * @param trackId the trackId
return -1L; */
} finally { private void cleanImport(long trackId) {
if (zipInputStream != null) { if (trackId != -1L) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != -1L) {
File dir = FileUtils.getPhotoDir(importTrackId);
if (FileUtils.isDirectory(dir)) {
for (File file : dir.listFiles()) {
file.delete();
}
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
}
}
/**
* Parses kml
*
* @param zipInputStream the zip input stream
* @return the imported track id or -1L
*/
private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null;
try { try {
zipInputStream.close(); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
} catch (IOException e) { byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
Log.e(TAG, "Unable to close zip input stream", e); return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
} }
}
}
}
/**
* Cleans up import.
*
* @param trackId the trackId
*/
private void cleanImport(long trackId) {
if (trackId != -1L) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
contentProviderUtils.deleteTrack(context, trackId);
} }
if (importTrackId != -1L) { /**
File dir = FileUtils.getPhotoDir(importTrackId); * Gets the kml as byte array.
if (FileUtils.isDirectory(dir)) { *
for (File file : dir.listFiles()) { * @param zipInputStream the zip input stream
file.delete(); */
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, count);
}
return byteArrayOutputStream.toByteArray();
} }
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
} }
}
/** /**
* Parses kml * Reads an image file.
* *
* @param zipInputStream the zip input stream * @param zipInputStream the zip input stream
* @return the imported track id or -1L * @param fileName the file name
*/ */
private long parseKml(ZipInputStream zipInputStream) throws IOException { private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
ByteArrayInputStream byteArrayInputStream = null; FileOutputStream fileOutputStream = null;
try { try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId); if (importTrackId == -1L) {
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream)); return;
return kmlFileTrackImporter.importFile(byteArrayInputStream); }
} finally { if (fileName.equals("")) {
if (byteArrayInputStream != null) { return;
byteArrayInputStream.close(); }
} File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName);
fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
FileUtils.updateMediaScanner(context, Uri.fromFile(file));
} finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
} }
}
/**
* Gets the kml as byte array.
*
* @param zipInputStream the zip input stream
*/
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, count);
}
return byteArrayOutputStream.toByteArray();
}
}
/**
* Reads an image file.
*
* @param zipInputStream the zip input stream
* @param fileName the file name
*/
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
FileOutputStream fileOutputStream = null;
try {
if (importTrackId == -1L) {
return;
}
if (fileName.equals("")) {
return;
}
File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName);
fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
FileUtils.updateMediaScanner(context, Uri.fromFile(file));
} finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
}
} }
@@ -24,11 +24,11 @@ import java.io.InputStream;
*/ */
public interface TrackImporter { public interface TrackImporter {
/** /**
* Import a file. * Import a file.
* *
* @param inputStream the file's input stream * @param inputStream the file's input stream
* @return the imported track id or -1L. * @return the imported track id or -1L.
*/ */
long importFile(InputStream inputStream); long importFile(InputStream inputStream);
} }
@@ -24,29 +24,29 @@ package com.google.android.apps.mytracks.services;
*/ */
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy { public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
private final long interval; private final long interval;
/** /**
* Constructor. * Constructor.
* *
* @param interval the interval to request for gps signal * @param interval the interval to request for gps signal
*/ */
public AbsoluteLocationListenerPolicy(long interval) { public AbsoluteLocationListenerPolicy(long interval) {
this.interval = interval; this.interval = interval;
} }
@Override @Override
public long getDesiredPollingInterval() { public long getDesiredPollingInterval() {
return interval; return interval;
} }
@Override @Override
public int getMinDistance() { public int getMinDistance() {
return 0; return 0;
} }
@Override @Override
public void updateIdleTime(long idleTime) { public void updateIdleTime(long idleTime) {
// Ignore // Ignore
} }
} }
@@ -26,48 +26,48 @@ package com.google.android.apps.mytracks.services;
*/ */
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy { public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
private final long minInterval; private final long minInterval;
private final long maxInterval; private final long maxInterval;
private final int minDistance; private final int minDistance;
// The time the user has been idle at the current location, in milliseconds. // The time the user has been idle at the current location, in milliseconds.
private long idleTime; private long idleTime;
/** /**
* Creates a policy that will be bounded by the given minInterval and * Creates a policy that will be bounded by the given minInterval and
* maxInterval. * maxInterval.
* *
* @param minInterval the smallest interval this policy will dictate, in * @param minInterval the smallest interval this policy will dictate, in
* milliseconds * milliseconds
* @param maxInterval the largest interval this policy will dictate, in * @param maxInterval the largest interval this policy will dictate, in
* milliseconds * milliseconds
* @param minDistance the minimum distance in meters * @param minDistance the minimum distance in meters
*/ */
public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) { public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) {
this.minInterval = minInterval; this.minInterval = minInterval;
this.maxInterval = maxInterval; this.maxInterval = maxInterval;
this.minDistance = minDistance; this.minDistance = minDistance;
} }
/* /*
* Returns an interval half of the idle time, but bounded by minInteval and * Returns an interval half of the idle time, but bounded by minInteval and
* maxInterval. * maxInterval.
*/ */
@Override @Override
public long getDesiredPollingInterval() { public long getDesiredPollingInterval() {
long desiredInterval = idleTime / 2; long desiredInterval = idleTime / 2;
// Round to second to avoid setting the interval too often // Round to second to avoid setting the interval too often
desiredInterval = (desiredInterval / 1000) * 1000; desiredInterval = (desiredInterval / 1000) * 1000;
return Math.max(Math.min(maxInterval, desiredInterval), minInterval); return Math.max(Math.min(maxInterval, desiredInterval), minInterval);
} }
@Override @Override
public int getMinDistance() { public int getMinDistance() {
return minDistance; return minDistance;
} }
@Override @Override
public void updateIdleTime(long newIdleTime) { public void updateIdleTime(long newIdleTime) {
idleTime = newIdleTime; idleTime = newIdleTime;
} }
} }
@@ -39,95 +39,95 @@ import com.google.android.maps.mytracks.R;
*/ */
public class ControlRecordingService extends IntentService implements ServiceConnection { public class ControlRecordingService extends IntentService implements ServiceConnection {
private static final String TAG = ControlRecordingService.class.getSimpleName(); private static final String TAG = ControlRecordingService.class.getSimpleName();
private ITrackRecordingService trackRecordingService; private ITrackRecordingService trackRecordingService;
private boolean connected = false; private boolean connected = false;
public ControlRecordingService() { public ControlRecordingService() {
super(ControlRecordingService.class.getSimpleName()); super(ControlRecordingService.class.getSimpleName());
}
@Override
public void onCreate() {
super.onCreate();
Intent newIntent = new Intent(this, TrackRecordingService.class);
startService(newIntent);
bindService(newIntent, this, 0);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
trackRecordingService = (ITrackRecordingService)service;
notifyConnected();
}
@Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}
/**
* Notifies all threads that connection to {@link TrackRecordingService} is
* available.
*/
private synchronized void notifyConnected() {
connected = true;
notifyAll();
}
/**
* Waits until the connection to {@link TrackRecordingService} is available.
*/
private synchronized void waitConnected() {
while (!connected) {
try {
wait();
} catch (InterruptedException e) {
// can safely ignore
}
} }
}
@Override @Override
protected void onHandleIntent(Intent intent) { public void onCreate() {
waitConnected(); super.onCreate();
onHandleIntent(intent, trackRecordingService);
}
Intent newIntent = new Intent(this, TrackRecordingService.class);
/** startService(newIntent);
* Handles the intent to start or stop a recording. bindService(newIntent, this, 0);
*
* @param intent to be handled
* @param service the trackRecordingService
*/
@VisibleForTesting
void onHandleIntent(Intent intent, ITrackRecordingService service) {
String action = intent.getAction();
if (action != null) {
if (action.equals(getString(R.string.track_action_start))) {
service.startNewTrack();
} else if (action.equals(getString(R.string.track_action_end))) {
service.endCurrentTrack();
} else if (action.equals(getString(R.string.track_action_pause))) {
service.pauseCurrentTrack();
} else if (action.equals(getString(R.string.track_action_resume))) {
service.resumeCurrentTrack();
}
} }
unbindService(this);
connected = false;
}
@Override @Override
public void onDestroy() { public void onServiceConnected(ComponentName name, IBinder service) {
super.onDestroy(); trackRecordingService = (ITrackRecordingService) service;
if (connected) { notifyConnected();
unbindService(this); }
connected = false;
@Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}
/**
* Notifies all threads that connection to {@link TrackRecordingService} is
* available.
*/
private synchronized void notifyConnected() {
connected = true;
notifyAll();
}
/**
* Waits until the connection to {@link TrackRecordingService} is available.
*/
private synchronized void waitConnected() {
while (!connected) {
try {
wait();
} catch (InterruptedException e) {
// can safely ignore
}
}
}
@Override
protected void onHandleIntent(Intent intent) {
waitConnected();
onHandleIntent(intent, trackRecordingService);
}
/**
* Handles the intent to start or stop a recording.
*
* @param intent to be handled
* @param service the trackRecordingService
*/
@VisibleForTesting
void onHandleIntent(Intent intent, ITrackRecordingService service) {
String action = intent.getAction();
if (action != null) {
if (action.equals(getString(R.string.track_action_start))) {
service.startNewTrack();
} else if (action.equals(getString(R.string.track_action_end))) {
service.endCurrentTrack();
} else if (action.equals(getString(R.string.track_action_pause))) {
service.pauseCurrentTrack();
} else if (action.equals(getString(R.string.track_action_resume))) {
service.resumeCurrentTrack();
}
}
unbindService(this);
connected = false;
}
@Override
public void onDestroy() {
super.onDestroy();
if (connected) {
unbindService(this);
connected = false;
}
} }
}
} }
@@ -27,87 +27,87 @@ import com.google.android.apps.mytracks.content.sensor.SensorState;
*/ */
public interface ITrackRecordingService { public interface ITrackRecordingService {
/** /**
* Starts gps. * Starts gps.
*/ */
void startGps(); void startGps();
/** /**
* Stops gps. * Stops gps.
*/ */
void stopGps(); void stopGps();
/** /**
* Starts recording a new track. * Starts recording a new track.
* *
* @return the track ID of the new track. * @return the track ID of the new track.
*/ */
long startNewTrack(); long startNewTrack();
/** /**
* Pauses the current recording track. * Pauses the current recording track.
*/ */
void pauseCurrentTrack(); void pauseCurrentTrack();
/** /**
* Resumes the current recording track. * Resumes the current recording track.
*/ */
void resumeCurrentTrack(); void resumeCurrentTrack();
/** /**
* Ends the current recording track. * Ends the current recording track.
*/ */
void endCurrentTrack(); void endCurrentTrack();
/** /**
* Returns true if currently recording a track. * Returns true if currently recording a track.
*/ */
boolean isRecording(); boolean isRecording();
/** /**
* Returns true if the current recording track is paused. Returns true if not recording. * Returns true if the current recording track is paused. Returns true if not recording.
*/ */
boolean isPaused(); boolean isPaused();
/** /**
* Gets the current recording track ID. Returns -1 if not recording. * Gets the current recording track ID. Returns -1 if not recording.
*/ */
long getRecordingTrackId(); long getRecordingTrackId();
/** /**
* Gets the total time for the current recording track. Returns 0 if not recording. * Gets the total time for the current recording track. Returns 0 if not recording.
*/ */
long getTotalTime(); long getTotalTime();
/** /**
* Inserts a waypoint in the current recording track. * Inserts a waypoint in the current recording track.
* *
* @param request the details of the waypoint to be inserted * @param request the details of the waypoint to be inserted
* @return the ID of the inserted waypoint * @return the ID of the inserted waypoint
*/ */
long insertWaypoint(WaypointCreationRequest request); long insertWaypoint(WaypointCreationRequest request);
/** /**
* Inserts a track point in the current recording track. * Inserts a track point in the current recording track.
* * <p>
* When recording a track, GPS locations are automatically inserted. This is used for * When recording a track, GPS locations are automatically inserted. This is used for
* inserting special track points or for testing. * inserting special track points or for testing.
* *
* @param location the track point to be inserted * @param location the track point to be inserted
*/ */
void insertTrackPoint(Location location); void insertTrackPoint(Location location);
/** /**
* Gets the current sensor data. Returns null if there is no data. * Gets the current sensor data. Returns null if there is no data.
*
* @return SensorDataSet object.
*/
SensorDataSet getSensorData();
* @return SensorDataSet object. /**
*/ * Gets the current sensor manager state.
SensorDataSet getSensorData(); * <p>
* return a Sensor.SensorState enum value.
/** */
* Gets the current sensor manager state. SensorState getSensorState();
*
* return a Sensor.SensorState enum value.
*/
SensorState getSensorState();
} }
@@ -25,23 +25,23 @@ package com.google.android.apps.mytracks.services;
*/ */
public interface LocationListenerPolicy { public interface LocationListenerPolicy {
/** /**
* Returns the polling interval this policy would like at this moment. * Returns the polling interval this policy would like at this moment.
* *
* @return the polling interval * @return the polling interval
*/ */
long getDesiredPollingInterval(); long getDesiredPollingInterval();
/** /**
* Returns the minimum distance between updates. * Returns the minimum distance between updates.
*/ */
int getMinDistance(); int getMinDistance();
/** /**
* Notifies the amount of time the user has been idle at his current location. * Notifies the amount of time the user has been idle at his current location.
* *
* @param idleTime the time that the user has been idle at his current * @param idleTime the time that the user has been idle at his current
* location * location
*/ */
void updateIdleTime(long idleTime); void updateIdleTime(long idleTime);
} }
@@ -44,11 +44,11 @@ import androidx.core.app.TaskStackBuilder;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.TrackDetailActivity; import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.TrackListActivity; import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.CustomContentProvider;
import com.google.android.apps.mytracks.content.ContentProviderUtils; import com.google.android.apps.mytracks.content.ContentProviderUtils;
import com.google.android.apps.mytracks.content.ContentProviderUtils.LocationIterator; import com.google.android.apps.mytracks.content.ContentProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.CustomContentProvider;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.SensorDataSetLocation;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType; import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
@@ -95,15 +95,11 @@ public class TrackRecordingService extends Service {
* Anything faster than that (in meters per second) will be considered moving. * Anything faster than that (in meters per second) will be considered moving.
*/ */
public static final double MAX_NO_MOVEMENT_SPEED = 0.224; public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private static final String TAG = TrackRecordingService.class.getSimpleName();
// 1 minute in milliseconds
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
@VisibleForTesting @VisibleForTesting
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private static final String TAG = TrackRecordingService.class.getSimpleName();
// 1 minute in milliseconds
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
// The following variables are set in onCreate: // The following variables are set in onCreate:
private ExecutorService executorService; private ExecutorService executorService;
private Context context; private Context context;
@@ -124,15 +120,6 @@ public class TrackRecordingService extends Service {
// The following variables are set when recording: // The following variables are set when recording:
private TripStatisticsUpdater trackTripStatisticsUpdater; private TripStatisticsUpdater trackTripStatisticsUpdater;
private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock;
private SensorManager sensorManager;
private Location lastLocation;
private boolean currentSegmentHasLocation;
private boolean isIdle; // true if idle
private ServiceBinder binder = new ServiceBinder(this);
/* /*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner * Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected. * class. Anonymous inner class will get garbage collected.
@@ -193,7 +180,13 @@ public class TrackRecordingService extends Service {
} }
} }
}; };
private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock;
private SensorManager sensorManager;
private Location lastLocation;
private boolean currentSegmentHasLocation;
private boolean isIdle; // true if idle
private ServiceBinder binder = new ServiceBinder(this);
private LocationListener locationListener = new LocationListener() { private LocationListener locationListener = new LocationListener() {
@Override @Override
@@ -16,9 +16,6 @@
package com.google.android.apps.mytracks.services; package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.BuildConfig;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -28,6 +25,9 @@ import android.os.IBinder.DeathRecipient;
import android.os.RemoteException; import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.BuildConfig;
/** /**
* Wrapper for the track recording service. This handles service * Wrapper for the track recording service. This handles service
* start/bind/unbind/stop. The service must be started before it can be bound. * start/bind/unbind/stop. The service must be started before it can be bound.
@@ -37,127 +37,124 @@ import android.util.Log;
*/ */
public class TrackRecordingServiceConnection { public class TrackRecordingServiceConnection {
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName(); private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
private final Context context;
private final Runnable callback;
private ITrackRecordingService trackRecordingService;
private final DeathRecipient deathRecipient = new DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "Service died.");
setTrackRecordingService(null);
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to the service.");
try {
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient.", e);
}
setTrackRecordingService((ITrackRecordingService) service);
}
private final DeathRecipient deathRecipient = new DeathRecipient() { @Override
@Override public void onServiceDisconnected(ComponentName className) {
public void binderDied() { Log.i(TAG, "Disconnected from the service.");
Log.d(TAG, "Service died."); setTrackRecordingService(null);
setTrackRecordingService(null); }
} };
};
private final ServiceConnection serviceConnection = new ServiceConnection() { /**
@Override * Constructor.
public void onServiceConnected(ComponentName className, IBinder service) { *
Log.i(TAG, "Connected to the service."); * @param context the context
try { * @param callback the callback to invoke when the service binding changes
service.linkToDeath(deathRecipient, 0); */
} catch (RemoteException e) { public TrackRecordingServiceConnection(Context context, Runnable callback) {
Log.e(TAG, "Failed to bind a death recipient.", e); this.context = context;
} this.callback = callback;
setTrackRecordingService((ITrackRecordingService)service);
} }
@Override /**
public void onServiceDisconnected(ComponentName className) { * Starts and binds the service.
Log.i(TAG, "Disconnected from the service."); */
setTrackRecordingService(null); public void startAndBind() {
} bindService(true);
};
private final Context context;
private final Runnable callback;
private ITrackRecordingService trackRecordingService;
/**
* Constructor.
*
* @param context the context
* @param callback the callback to invoke when the service binding changes
*/
public TrackRecordingServiceConnection(Context context, Runnable callback) {
this.context = context;
this.callback = callback;
}
/**
* Starts and binds the service.
*/
public void startAndBind() {
bindService(true);
}
/**
* Binds the service if it is started.
*/
public void bindIfStarted() {
bindService(false);
}
/**
* Unbinds and stops the service.
*/
public void unbindAndStop() {
unbind();
context.stopService(new Intent(context, TrackRecordingService.class));
}
/**
* Unbinds the service (but leave it running).
*/
public void unbind() {
try {
context.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// Means not bound to the service. OK to ignore.
}
setTrackRecordingService(null);
}
/**
* Gets the track recording service if bound. Returns null otherwise
*/
public ITrackRecordingService getServiceIfBound() {
return trackRecordingService;
}
/**
* Binds the service if it is started.
*
* @param startIfNeeded start the service if needed
*/
private void bindService(boolean startIfNeeded) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
} }
if (!startIfNeeded /**
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) { * Binds the service if it is started.
Log.d(TAG, "Service is not started. Not binding it."); */
return; public void bindIfStarted() {
bindService(false);
} }
if (startIfNeeded) { /**
Log.i(TAG, "Starting the service."); * Unbinds and stops the service.
context.startService(new Intent(context, TrackRecordingService.class)); */
public void unbindAndStop() {
unbind();
context.stopService(new Intent(context, TrackRecordingService.class));
} }
Log.i(TAG, "Binding the service."); /**
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0; * Unbinds the service (but leave it running).
context.bindService(new Intent(context, TrackRecordingService.class), serviceConnection, flags); */
} public void unbind() {
try {
/** context.unbindService(serviceConnection);
* Sets the trackRecordingService. } catch (IllegalArgumentException e) {
* // Means not bound to the service. OK to ignore.
* @param value the value }
*/ setTrackRecordingService(null);
private void setTrackRecordingService(ITrackRecordingService value) { }
trackRecordingService = value;
if (callback != null) { /**
callback.run(); * Gets the track recording service if bound. Returns null otherwise
*/
public ITrackRecordingService getServiceIfBound() {
return trackRecordingService;
}
/**
* Binds the service if it is started.
*
* @param startIfNeeded start the service if needed
*/
private void bindService(boolean startIfNeeded) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
if (!startIfNeeded
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
Log.d(TAG, "Service is not started. Not binding it.");
return;
}
if (startIfNeeded) {
Log.i(TAG, "Starting the service.");
context.startService(new Intent(context, TrackRecordingService.class));
}
Log.i(TAG, "Binding the service.");
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(new Intent(context, TrackRecordingService.class), serviceConnection, flags);
}
/**
* Sets the trackRecordingService.
*
* @param value the value
*/
private void setTrackRecordingService(ITrackRecordingService value) {
trackRecordingService = value;
if (callback != null) {
callback.run();
}
} }
}
} }
@@ -41,15 +41,13 @@ import java.util.UUID;
*/ */
public class BluetoothConnectionManager { public class BluetoothConnectionManager {
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
// Message types sent to handler // Message types sent to handler
public static final int MESSAGE_DEVICE_NAME = 1; public static final int MESSAGE_DEVICE_NAME = 1;
public static final int MESSAGE_READ = 2; public static final int MESSAGE_READ = 2;
public static final String KEY_DEVICE_NAME = "device_name"; public static final String KEY_DEVICE_NAME = "device_name";
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final Context context; private final Context context;

Some files were not shown because too many files have changed in this diff Show More