Compare commits

..

10 Commits

Author SHA1 Message Date
Dennis Guse 597e1b2c7f Release: v4.25.2 2026-02-09 07:51:06 +01:00
jimkats 8156993921 Translated using Weblate (Greek)
Currently translated at 51.0% (278 of 545 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-02-08 22:49:35 +01:00
Dennis Guse 9890dc28bd Dashboard API: provide version 2 again (required by OSMDashboard until v6.4.1).
Part of #2265.
2026-02-08 22:48:43 +01:00
Dennis Guse b65d83010d Bugfix: ShareContentProvider should report contentType for all Uris (not only TrackFileFormat).
Part of #2265.
2026-02-08 18:11:52 +01:00
Dennis Guse 1a5d06f9c6 Cleanup: renamed IntentDashboardUtils.java to DataProvider. 2026-02-07 19:20:48 +01:00
Dennis Guse 723c21d7ec Dashboard API: add tests for column names.
Part of #2265.
2026-02-07 09:39:23 +01:00
Dennis Guse 8fe0c3be7d Bugfix: update data for UI immediately after starting recording. 2026-02-06 18:54:32 +01:00
Dennis Guse e64792bf3f Resetting preferences requires clearing and naming each preference file explicitly. 2026-02-06 18:54:18 +01:00
Dennis Guse 21bafff761 TrackRecordingService should start UI timer at the end of startRecording. 2026-02-06 18:54:06 +01:00
Dennis Guse c33c2de51d Cleanup: EspressoUI should always lookup UI elements. 2026-02-06 18:53:55 +01:00
21 changed files with 627 additions and 234 deletions
+2 -2
View File
@@ -47,8 +47,8 @@ android {
defaultConfig { defaultConfig {
applicationId 'de.dennisguse.opentracks' applicationId 'de.dennisguse.opentracks'
versionCode 6554 versionCode 6564
versionName "v4.25.1" versionName "v4.25.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -0,0 +1,8 @@
v4.25.2: OpenTracks
Changes:
- Restored compatibility with OSMDashboard (Dashboard API protocol version 2)
Bugfix:
- Update UI immediately after resuming a track
@@ -36,7 +36,8 @@ public class EspressoDeleteTrackTest {
@Test @Test
public void espressoDeleteTrackTest() { public void espressoDeleteTrackTest() {
// TrackListActivity: start recording // TrackListActivity: start recording
onView(withId(R.id.track_list_fab_action)).perform(click()); onView(withId(R.id.track_list_fab_action))
.perform(click());
// TrackRecordingActivity // TrackRecordingActivity
onView(withId(R.id.track_recording_fab_action)) onView(withId(R.id.track_recording_fab_action))
@@ -46,10 +47,12 @@ public class EspressoDeleteTrackTest {
.perform(longClick()); .perform(longClick());
// TrackStoppedActivity // TrackStoppedActivity
onView(withId(R.id.finish_button)).perform(click()); onView(withId(R.id.finish_button))
.perform(click());
// select track // select track
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick()); onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0)))
.perform(longClick());
// open menu and delete selected track // open menu and delete selected track
onView( onView(
@@ -62,9 +65,11 @@ public class EspressoDeleteTrackTest {
isDisplayed())) isDisplayed()))
.perform(click()); .perform(click());
onView(withText("Delete")).perform(click()); onView(withText("Delete"))
.perform(click());
onView(withText("OK")).perform(click()); onView(withText("OK"))
.perform(click());
// tracklist is empty now // tracklist is empty now
onView(allOf(withText("Start recording your next adventure here"), isDisplayed())); onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
@@ -15,7 +15,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor; import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
@@ -45,41 +44,43 @@ public class EspressoEditTrackRecordingTest {
public void espressoEditTrackRecordingTest() { public void espressoEditTrackRecordingTest() {
{ {
// TrackListActivity: start recording // TrackListActivity: start recording
ViewInteraction fabRecordButton = onView(withId(R.id.track_list_fab_action)); onView(withId(R.id.track_list_fab_action))
fabRecordButton.perform(click()); .perform(click());
} }
{ {
// TrackRecordingActivity // TrackRecordingActivity
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
// wait; stay recording // wait; stay recording
trackControllerStopButton.perform(waitFor(4000)); onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(4000));
// open menu // open menu
openContextualActionModeOverflowMenu(); openContextualActionModeOverflowMenu();
// Click the item. // Click the item.
onView(withText(R.string.menu_edit)).perform(click()); onView(withText(R.string.menu_edit))
.perform(click());
// change name for "New Name" // change name for "New Name"
ViewInteraction textInputEditText = onView(withId(R.id.track_edit_name)); onView(withId(R.id.track_edit_name))
textInputEditText.perform(scrollTo(), replaceText("New Name")); .perform(scrollTo(), replaceText("New Name"));
ViewInteraction textInputEditText2 = onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed())); onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()))
textInputEditText2.perform(closeSoftKeyboard()); .perform(closeSoftKeyboard());
// save edition // save edition
ViewInteraction appCompatButton = onView(withId(R.id.track_edit_save)); onView(withId(R.id.track_edit_save))
appCompatButton.perform(click()); .perform(click());
// stop; // stop;
trackControllerStopButton.perform(longClick()); onView(withId(R.id.track_recording_fab_action))
.perform(longClick());
// it's on track stopped activity and there are two buttons // it's on track stopped activity and there are two buttons
ViewInteraction resumeButton = onView(withId(R.id.resume_button)); onView(withId(R.id.resume_button))
resumeButton.check(matches(isDisplayed())); .check(matches(isDisplayed()));
ViewInteraction finishButton = onView(withId(R.id.finish_button)); onView(withId(R.id.finish_button))
finishButton.check(matches(isDisplayed())); .check(matches(isDisplayed()));
} }
} }
} }
@@ -37,8 +37,8 @@ public class EspressoUITest {
public void record_stop_resume_stop_finish() { public void record_stop_resume_stop_finish() {
{ {
// TrackListActivity: start recording // TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action)); onView(withId(R.id.track_list_fab_action))
trackControllerRecordButton.perform(click()); .perform(click());
} }
{ {
// TrackRecordingActivity: wait to record some time and then stop // TrackRecordingActivity: wait to record some time and then stop
@@ -66,13 +66,12 @@ public class EspressoUITest {
public void record_move_through_tabs() { public void record_move_through_tabs() {
{ {
// TrackListActivity: start recording // TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action)); onView(withId(R.id.track_list_fab_action))
trackControllerRecordButton.perform(click()); .perform(click());
} }
{ {
// TrackRecordingActivity // TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout)); ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
tabLayout.perform(selectTabAtIndex(1)); tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000)); tabLayout.perform(waitFor(1000));
@@ -87,7 +86,8 @@ public class EspressoUITest {
tabLayout.perform(waitFor(1000)); tabLayout.perform(waitFor(1000));
// stop // stop
trackControllerStopButton.perform(longClick()); onView(withId(R.id.track_recording_fab_action)).
perform(longClick());
} }
} }
@@ -0,0 +1,224 @@
package de.dennisguse.opentracks.publicapi;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@RunWith(AndroidJUnit4.class)
public class DataProviderTest {
private final ContentResolver resolver = ApplicationProvider.getApplicationContext().getContentResolver();
@Test
public void supportsVersion2_tracks() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = new String[]{
"_id",
"name",
"description",
"category",
"activity_type_localized",
"starttime",
"stoptime",
"totaltime",
"movingtime",
"totaldistance",
"maxspeed",
"avgspeed",
"avgmovingspeed",
"minelevation",
"maxelevation",
"elevationgain",
"elevationloss"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTracksColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion2_trackpoints() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
"latitude",
"longitude",
"time",
"type",
"speed"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTrackpointsColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion2_markers() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
"description",
"category",
"trackid",
"longitude",
"latitude",
"photoUrl",
"icon"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedMarkersColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_tracks() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/1");
String[] expectedTracksColumns = {
"_id",
"name",
"description",
"activity_type",
"activity_type_localized",
"time_start",
"time_stop",
"duration_total",
"duration_moving",
"distance",
"speed_max",
"altitude_min",
"altitude_max",
"altitude_gain",
"altitude_loss"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTracksColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_trackpoints() {
//given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
String[] expectedTrackpointsColumns = {
"_id",
"trackid",
"latitude",
"longitude",
"time",
"type",
"speed"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedTrackpointsColumns,
null,
null,
null
)) {
//then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
}
}
@Test
public void supportsVersion3_markers() {
// given
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/1");
String[] expectedMarkersColumns = {
"_id",
"name",
"description",
"marker_type_localized",
"trackid",
"latitude",
"longitude",
"photoUrl"
};
// when
try (Cursor cursor = resolver.query(
uri,
expectedMarkersColumns,
null,
null,
null
)) {
// then
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
assertNotNull(cursor);
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
}
}
}
@@ -52,7 +52,6 @@ import de.dennisguse.opentracks.sensors.GpsStatusValue;
/** /**
* Testing the states of TrackRecordingService. * Testing the states of TrackRecordingService.
* As states are checked against MutableLiveData that are updated via `postValue()` (is asynchronous to be inform other Threads), Thread.sleep() is required.
*/ */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceStateMachineTest { public class TrackRecordingServiceStateMachineTest {
@@ -118,13 +117,12 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest @MediumTest
@Test @Test
public void gps_startStop() throws InterruptedException { public void gps_startStop() {
// given // given
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
// when // when
service.tryStartSensors(); service.tryStartSensors();
Thread.sleep(1000);
// then // then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -133,7 +131,6 @@ public class TrackRecordingServiceStateMachineTest {
// when // when
service.stopSensors(); service.stopSensors();
Thread.sleep(1000);
// then // then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -143,13 +140,12 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest @MediumTest
@Test @Test
public void recording_startStopResume_no_data() throws InterruptedException { public void recording_startStopResume_no_data() {
// given // given
assertFalse(service.isRecording()); assertFalse(service.isRecording());
// when // when
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
Thread.sleep(1000);
// then // then
assertTrue(service.isRecording()); assertTrue(service.isRecording());
@@ -161,7 +157,6 @@ public class TrackRecordingServiceStateMachineTest {
// when // when
service.resumeTrack(trackId); service.resumeTrack(trackId);
Thread.sleep(1000);
// then // then
assertTrue(service.isRecording()); assertTrue(service.isRecording());
@@ -172,7 +167,6 @@ public class TrackRecordingServiceStateMachineTest {
// when // when
service.endCurrentTrack(); service.endCurrentTrack();
Thread.sleep(1000);
// then // then
assertFalse(service.isRecording()); assertFalse(service.isRecording());
@@ -183,12 +177,10 @@ public class TrackRecordingServiceStateMachineTest {
// when // when
service.resumeTrack(trackId); service.resumeTrack(trackId);
Thread.sleep(1000);
// then // then
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue()); assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue()); assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
} }
@@ -214,7 +206,7 @@ public class TrackRecordingServiceStateMachineTest {
@MediumTest @MediumTest
@Test @Test
public void cannotEnd_without_starting() throws InterruptedException { public void cannotEnd_without_starting() {
// given // given
assertFalse(service.isRecording()); assertFalse(service.isRecording());
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -223,7 +215,6 @@ public class TrackRecordingServiceStateMachineTest {
// when // when
service.endCurrentTrack(); service.endCurrentTrack();
Thread.sleep(1000);
// then // then
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue()); assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
@@ -234,33 +225,30 @@ public class TrackRecordingServiceStateMachineTest {
@Ignore("TODO Bug: GPS can be stopped although the current track is recording") @Ignore("TODO Bug: GPS can be stopped although the current track is recording")
@MediumTest @MediumTest
@Test @Test
public void recording_stopGPS_noop() throws InterruptedException { public void recording_stopGPS_noop() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
// when // when
service.stopSensors(); //TODO Should be ignored as service is recording service.stopSensors(); //TODO Should be ignored as service is recording
Thread.sleep(1000);
// then // then
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue()); assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue()); assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue()); assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
} }
@MediumTest @MediumTest
@Test @Test
public void recording_startRecording_alreadyRecording() throws InterruptedException { public void recording_startRecording_alreadyRecording() {
// given // given
Track.Id trackId = service.startNewTrack(); Track.Id trackId = service.startNewTrack();
assertTrue(service.isRecording()); assertTrue(service.isRecording());
// when // when
Track.Id newTrackId = service.startNewTrack(); Track.Id newTrackId = service.startNewTrack();
Thread.sleep(1000);
// then // then
assertNotNull(trackId); assertNotNull(trackId);
@@ -14,7 +14,6 @@ import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import android.util.Pair; import android.util.Pair;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -61,28 +60,34 @@ public class EspressoAggregatedFilterTest {
@Test @Test
public void espressoAggregatedFilterTest() { public void espressoAggregatedFilterTest() {
// open AggregatedStatisticsActivity through toolbar's menu item // open AggregatedStatisticsActivity through toolbar's menu item
onView(ViewMatchers.withId(R.id.bottom_app_bar)).check(matches(isDisplayed())); onView(ViewMatchers.withId(R.id.bottom_app_bar))
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_stats_button)))); .check(matches(isDisplayed()));
onView(withId(R.id.aggregated_stats_button)).perform(click()); onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
onView(withId(R.id.aggregated_stats_button))
.perform(click());
// open FilterDialogFragment through toolbar's menu item // open FilterDialogFragment through toolbar's menu item
onView(withId(R.id.bottom_app_bar)).check(matches(isDisplayed())); onView(withId(R.id.bottom_app_bar))
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter)))); .check(matches(isDisplayed()));
onView(withId(R.id.aggregated_statistics_filter)).perform(click()); onView(withId(R.id.bottom_app_bar))
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter))
.perform(click());
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text // check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
ViewInteraction checkBox = onView( onView(allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED), withParent(allOf(withId(R.id.filter_items),
withParent(allOf(withId(R.id.filter_items), withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))), isDisplayed()))
isDisplayed())); .perform(waitFor(2000));
checkBox.perform(waitFor(2000));
// check there's an edit text for "from date" // check there's an edit text for "from date"
onView(withId(R.id.filter_date_edit_text_from)).check(matches(isDisplayed())); onView(withId(R.id.filter_date_edit_text_from))
.check(matches(isDisplayed()));
// check there's an edit text for "to date" // check there's an edit text for "to date"
onView(withId(R.id.filter_date_edit_text_to)).check(matches(isDisplayed())); onView(withId(R.id.filter_date_edit_text_to))
.check(matches(isDisplayed()));
} }
} }
@@ -47,6 +47,7 @@ import java.util.Objects;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackListBinding; import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.sensors.GpsStatusValue; import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.services.MissingPermissionException; import de.dennisguse.opentracks.services.MissingPermissionException;
import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.services.RecordingStatus;
@@ -62,7 +63,6 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
import de.dennisguse.opentracks.ui.markers.MarkerListActivity; import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils; import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.RecyclerViewSwipeDeleteCallback; import de.dennisguse.opentracks.ui.util.RecyclerViewSwipeDeleteCallback;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionRequester; import de.dennisguse.opentracks.util.PermissionRequester;
@@ -367,7 +367,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
} }
if (itemId == R.id.list_context_menu_show_on_map) { if (itemId == R.id.list_context_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, false, trackIds); DataProvider.showTrackOnMap(this, false, trackIds);
return true; return true;
} }
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding; import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
@@ -45,7 +46,6 @@ import de.dennisguse.opentracks.share.ShareUtils;
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment; import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity; import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
/** /**
@@ -167,7 +167,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
} }
if (item.getItemId() == R.id.track_detail_menu_show_on_map) { if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, false, trackId); DataProvider.showTrackOnMap(this, false, trackId);
return true; return true;
} }
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.TrackRecordingBinding; import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.sensors.GpsStatusValue; import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.services.RecordingStatus; import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
@@ -49,7 +50,6 @@ import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity; import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity; import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.ui.util.ActivityUtils; import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
/** /**
@@ -267,7 +267,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) { if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.showTrackOnMap(this, true, trackId); DataProvider.showTrackOnMap(this, true, trackId);
return true; return true;
} }
@@ -69,9 +69,11 @@ public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName(); private static final String TAG = ContentProviderUtils.class.getSimpleName();
//TODO Move to CustomContentProvider
// The authority (the first part of the URI) for the app's content provider. // The authority (the first part of the URI) for the app's content provider.
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content"; static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
//TODO Move to CustomContentProvider
// The base URI for the app's content provider. // The base URI for the app's content provider.
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE; public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns; import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns; import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns; import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.publicapi.DataProvider;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
/** /**
@@ -94,7 +95,6 @@ public class CustomContentProvider extends ContentProvider {
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")"; "AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
public CustomContentProvider() { public CustomContentProvider() {
//TODO Add tests for used URIs (incl. Dashboard API)
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
@@ -108,6 +108,10 @@ public class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKS_BY_IDS.getPath() + "*", UrlType.DASHBOARD_TRACKS_BY_IDS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.ordinal());
} }
@Override @Override
@@ -170,12 +174,21 @@ public class CustomContentProvider extends ContentProvider {
@Override @Override
public String getType(@NonNull Uri url) { public String getType(@NonNull Uri url) {
String contentType = getUrlType(url).contentType;
if (contentType != null) {
return contentType;
}
//TODO replace with getUrlType(url).getContentType()
return switch (getUrlType(url)) { return switch (getUrlType(url)) {
case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE; case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE; case TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS -> TracksColumns.CONTENT_TYPE; case TRACKS -> TracksColumns.CONTENT_TYPE;
case MARKERS -> MarkerColumns.CONTENT_TYPE; case MARKERS -> MarkerColumns.CONTENT_TYPE;
case MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE; case MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
case DASHBOARD_TRACKS_BY_IDS -> UrlType.DASHBOARD_TRACKS_BY_IDS.contentType;
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.contentType;
case DASHBOARD_MARKERS_BY_TRACKIDS -> UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.contentType;
default -> throw new IllegalArgumentException("Unknown URL " + url); default -> throw new IllegalArgumentException("Unknown URL " + url);
}; };
} }
@@ -225,6 +238,7 @@ public class CustomContentProvider extends ContentProvider {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null; String sortOrder = null;
String groupBy = null; String groupBy = null;
//TODO queryBuilder.setStrict(true);
//TODO queryBuilder.appendWhere() should use selectionArgs? //TODO queryBuilder.appendWhere() should use selectionArgs?
switch (getUrlType(url)) { switch (getUrlType(url)) {
case TRACKPOINTS -> { case TRACKPOINTS -> {
@@ -239,6 +253,11 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
} }
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKPOINTS);
}
case TRACKS -> { case TRACKS -> {
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) { if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)"); queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
@@ -251,6 +270,11 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TracksColumns.TABLE_NAME); queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
} }
case DASHBOARD_TRACKS_BY_IDS -> {
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKS);
}
case TRACKS_SENSOR_STATS -> { case TRACKS_SENSOR_STATS -> {
//TODO Replace with SQLQueryBuilder? //TODO Replace with SQLQueryBuilder?
long trackId = ContentUris.parseId(url); long trackId = ContentUris.parseId(url);
@@ -281,6 +305,11 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
} }
case DASHBOARD_MARKERS_BY_TRACKIDS -> {
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_MARKERS);
}
default -> throw new IllegalArgumentException("Unknown url " + url); default -> throw new IllegalArgumentException("Unknown url " + url);
} }
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder); Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder);
@@ -372,9 +401,11 @@ public class CustomContentProvider extends ContentProvider {
throw new SQLException("Failed to insert a marker " + url); throw new SQLException("Failed to insert a marker " + url);
} }
private enum UrlType { //TODO Add ContentType as attribute
public enum UrlType {
TRACKPOINTS, TRACKPOINTS,
TRACKPOINTS_BY_ID, TRACKPOINTS_BY_ID,
//TODO Check if still used
TRACKPOINTS_BY_TRACKID, TRACKPOINTS_BY_TRACKID,
TRACKS, TRACKS,
TRACKS_BY_ID, TRACKS_BY_ID,
@@ -382,6 +413,36 @@ public class CustomContentProvider extends ContentProvider {
TRACKS_AGGREGATED_STATISTICS, TRACKS_AGGREGATED_STATISTICS,
MARKERS, MARKERS,
MARKERS_BY_ID, MARKERS_BY_ID,
MARKERS_BY_TRACKID @Deprecated //TODO Should not be used anymore
MARKERS_BY_TRACKID,
DASHBOARD_TRACKS_BY_IDS(Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/tracks/"), TracksColumns.CONTENT_TYPE),
DASHBOARD_TRACKPOINTS_BY_TRACKIDS(Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/trackpoints/"), TrackPointsColumns.CONTENT_TYPE),
DASHBOARD_MARKERS_BY_TRACKIDS(Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/dashboard/markers/"), MarkerColumns.CONTENT_TYPE);
private final Uri uri;
private final String contentType;
//TODO Add Uri as attribute:
//TODO Add ContentType as attribute:
@Deprecated()
UrlType() {
this.uri = null;
this.contentType = null;
}
UrlType(Uri uri, String contentType) {
this.uri = uri;
this.contentType = contentType;
}
public Uri getUri() {
return uri;
}
String getPath() {
return uri.getPath();
}
} }
} }
@@ -89,11 +89,10 @@ public class ShareContentProvider extends CustomContentProvider {
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER)); trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename)); Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename));
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri + " with MIME " + mime); Log.d(TAG, "Created uri " + uri + " with MIME " + trackFileFormat.getMimeType());
return new Pair<>(uri, mime); return new Pair<>(uri, trackFileFormat.getMimeType());
} }
static Set<Track.Id> parseURI(Uri uri) { static Set<Track.Id> parseURI(Uri uri) {
@@ -126,6 +125,7 @@ public class ShareContentProvider extends CustomContentProvider {
} }
} }
@Nullable
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) { private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
return switch (uriMatcher.match(uri)) { return switch (uriMatcher.match(uri)) {
case URI_GPX -> TrackFileFormat.GPX; case URI_GPX -> TrackFileFormat.GPX;
@@ -134,16 +134,10 @@ public class ShareContentProvider extends CustomContentProvider {
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES -> case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES ->
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES; TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
case URI_CSV -> TrackFileFormat.CSV; case URI_CSV -> TrackFileFormat.CSV;
default -> default -> null;
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
}; };
} }
@Nullable
private static String getTypeMime(@NonNull Uri uri) {
return getTrackFileFormat(uri).getMimeType();
}
@Override @Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
if (uriMatcher.match(uri) == -1) { if (uriMatcher.match(uri) == -1) {
@@ -182,9 +176,9 @@ public class ShareContentProvider extends CustomContentProvider {
@Nullable @Nullable
@Override @Override
public String getType(@NonNull Uri uri) { public String getType(@NonNull Uri uri) {
String mime = getTypeMime(uri); TrackFileFormat trackFileFormat = getTrackFileFormat(uri);
if (mime != null) { if (trackFileFormat != null) {
return mime; return trackFileFormat.getMimeType();
} }
return super.getType(uri); return super.getType(uri);
@@ -5,7 +5,6 @@ import android.content.res.Resources;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@@ -31,11 +30,13 @@ public enum TrackFileFormat {
return new KMLTrackExporter(context, contentProviderUtils, false); return new KMLTrackExporter(context, contentProviderUtils, false);
} }
@NonNull
@Override @Override
public String getMimeType() { public String getMimeType() {
return "application/vnd.google-earth.kml+xml"; return "application/vnd.google-earth.kml+xml";
} }
@NonNull
public String getExtension() { public String getExtension() {
return "kml"; return "kml";
} }
@@ -50,11 +51,13 @@ public enum TrackFileFormat {
return new KMZTrackExporter(context, contentProviderUtils, exportPhotos); return new KMZTrackExporter(context, contentProviderUtils, exportPhotos);
} }
@NonNull
@Override @Override
public String getMimeType() { public String getMimeType() {
return "application/vnd.google-earth.kmz"; return "application/vnd.google-earth.kmz";
} }
@NonNull
public String getExtension() { public String getExtension() {
return "kmz"; return "kmz";
} }
@@ -72,11 +75,13 @@ public enum TrackFileFormat {
return new GPXTrackExporter(contentProviderUtils, context.getString(R.string.app_name)); return new GPXTrackExporter(contentProviderUtils, context.getString(R.string.app_name));
} }
@NonNull
@Override @Override
public String getMimeType() { public String getMimeType() {
return "application/gpx+xml"; return "application/gpx+xml";
} }
@NonNull
public String getExtension() { public String getExtension() {
return "gpx"; return "gpx";
} }
@@ -88,11 +93,13 @@ public enum TrackFileFormat {
return new CSVTrackExporter(contentProviderUtils); return new CSVTrackExporter(contentProviderUtils);
} }
@NonNull
@Override @Override
public String getMimeType() { public String getMimeType() {
return "text/csv"; return "text/csv";
} }
@NonNull
@Override @Override
public String getExtension() { public String getExtension() {
return "csv"; return "csv";
@@ -115,16 +122,10 @@ public enum TrackFileFormat {
return preferenceIdLabelMap; return preferenceIdLabelMap;
} }
public static TrackFileFormat valueOfPreferenceId(final String preferenceId) {
return Arrays.stream(values())
.filter(trackFileFormat -> trackFileFormat.getPreferenceId().equals(preferenceId))
.findFirst()
.orElse(null);
}
/** /**
* Returns the mime type for each format. * Returns the mime type for each format.
*/ */
@NonNull
public abstract String getMimeType(); public abstract String getMimeType();
/** /**
@@ -137,6 +138,7 @@ public enum TrackFileFormat {
/** /**
* Returns the file extension for each format. * Returns the file extension for each format.
*/ */
@NonNull
public abstract String getExtension(); public abstract String getExtension();
/** /**
@@ -149,6 +151,7 @@ public enum TrackFileFormat {
/** /**
* The identifier to be stored in the preferences. * The identifier to be stored in the preferences.
*/ */
@NonNull
public String getPreferenceId() { public String getPreferenceId() {
return preferenceId; return preferenceId;
} }
@@ -0,0 +1,199 @@
package de.dennisguse.opentracks.publicapi;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Map;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.CustomContentProvider;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Create an {@link Intent} to request showing tracks on a Map or a Dashboard.
* The receiving {@link android.app.Activity} gets temporary access to the {@link TracksColumns} and the {@link TrackPointsColumns} (incl. update).
*/
public class DataProvider {
private static final String TAG = DataProvider.class.getSimpleName();
private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
/**
* Assume "1" if not present.
*/
private static final String EXTRAS_PROTOCOL_VERSION = "PROTOCOL_VERSION";
/**
* version 1: the initial version.
* version 2: replaced pause/resume trackpoints for track segmentation (lat=100 / lat=200) by TrackPoint.Type.
* version 3: renamed track database columns.
*/
private static final int CURRENT_VERSION = 2;
private static final String EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK = "EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK";
private static final String EXTRAS_SHOULD_KEEP_SCREEN_ON = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_WHEN_LOCKED = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_FULLSCREEN = "EXTRAS_SHOULD_FULLSCREEN";
private static final int TRACK_URI_INDEX = 0;
private static final int TRACKPOINTS_URI_INDEX = 1;
private static final int MARKERS_URI_INDEX = 2;
public static final Map<String, String> DATA_PROJECTIONMAP_TRACKS = Map.ofEntries(
//V2
Map.entry("_id", TracksColumns._ID),
Map.entry("name", TracksColumns.NAME),
Map.entry("description", TracksColumns.DESCRIPTION),
Map.entry("category", TracksColumns.ACTIVITY_TYPE + " AS category"),
Map.entry("activity_type_localized", TracksColumns.ACTIVITY_TYPE_LOCALIZED),
Map.entry("starttime", TracksColumns.STARTTIME + " AS starttime"),
Map.entry("stoptime", TracksColumns.STOPTIME + " AS stoptime"),
Map.entry("totaltime", TracksColumns.TOTALTIME + " AS totaltime"),
Map.entry("movingtime", TracksColumns.MOVINGTIME + " AS movingtime"),
Map.entry("totaldistance", TracksColumns.TOTALDISTANCE + " AS totaldistance"),
Map.entry("maxspeed", TracksColumns.MAXSPEED + " AS maxspeed"),
Map.entry("avgspeed", "null AS avgspeed"),
Map.entry("avgmovingspeed", "null AS avgmovingspeed"),
Map.entry("minelevation", TracksColumns.MIN_ALTITUDE + " AS minelevation"),
Map.entry("maxelevation", TracksColumns.MAX_ALTITUDE + " AS maxelevation"),
Map.entry("elevationgain", TracksColumns.ALTITUDE_GAIN + " AS elevationgain"),
Map.entry("elevationloss", TracksColumns.ALTITUDE_LOSS + " AS elevationloss"),
//V3
// Map.entry("_id", TracksColumns._ID),
// Map.entry("name",TracksColumns.NAME),
// Map.entry("description",TracksColumns.DESCRIPTION),
Map.entry("activity_type", TracksColumns.ACTIVITY_TYPE),
// Map.entry("activity_type_localized",TracksColumns.ACTIVITY_TYPE_LOCALIZED),
Map.entry("time_start", TracksColumns.STARTTIME),
Map.entry("time_stop", TracksColumns.STOPTIME),
Map.entry("duration_total", TracksColumns.TOTALTIME),
Map.entry("duration_moving", TracksColumns.MOVINGTIME),
Map.entry("distance", TracksColumns.TOTALDISTANCE),
Map.entry("speed_max", TracksColumns.MAXSPEED),
Map.entry("altitude_min", TracksColumns.MIN_ALTITUDE),
Map.entry("altitude_max", TracksColumns.MAX_ALTITUDE),
Map.entry("altitude_gain", TracksColumns.ALTITUDE_GAIN),
Map.entry("altitude_loss", TracksColumns.ALTITUDE_LOSS)
);
public static final Map<String, String> DATA_PROJECTIONMAP_TRACKPOINTS = Map.ofEntries(
//V2 and V3
Map.entry("_id", TrackPointsColumns._ID),
Map.entry("trackid", TrackPointsColumns.TRACKID),
Map.entry("latitude", TrackPointsColumns.LATITUDE),
Map.entry("longitude", TrackPointsColumns.LONGITUDE),
Map.entry("time", TrackPointsColumns.TIME),
Map.entry("type", TrackPointsColumns.TYPE),
Map.entry("speed", TrackPointsColumns.SPEED)
);
public static final Map<String, String> DATA_PROJECTIONMAP_MARKERS = Map.ofEntries(
//V2
Map.entry("_id", MarkerColumns._ID),
Map.entry("trackid", MarkerColumns.TRACKID),
Map.entry("name", MarkerColumns.NAME),
Map.entry("description", MarkerColumns.DESCRIPTION),
Map.entry("category", MarkerColumns.TYPE_LOCALIZED + " AS category"),
Map.entry("latitude", MarkerColumns.LATITUDE),
Map.entry("longitude", MarkerColumns.LONGITUDE),
Map.entry("photoUrl", MarkerColumns.PHOTOURL),
Map.entry("icon", "null AS icon"),
//V3
// Map.entry("_id", MarkerColumns._ID),
// Map.entry("trackid",MarkerColumns.TRACKID),
// Map.entry("name",MarkerColumns.NAME),
// Map.entry("description",MarkerColumns.DESCRIPTION),
Map.entry("marker_type_localized", MarkerColumns.TYPE_LOCALIZED)
// Map.entry("latitude",MarkerColumns.LATITUDE),
// Map.entry("longitude",MarkerColumns.LONGITUDE),
// Map.entry("photoUrl",MarkerColumns.PHOTOURL)
);
private DataProvider() {
}
/**
* Send intent to show tracks on a map (needs an another app) as resource URIs.
*
* @param context the context
* @param isRecording are we currently recording?
* @param trackIds the track ids
*/
public static void showTrackOnMap(Context context, boolean isRecording, Track.Id... trackIds) {
startDashboard(context, isRecording, null, null, trackIds);
}
/**
* Send intent to show tracks on a map (needs an another app) as resource URIs.
* By providing a targetPackage and targetClass an explicit intent can be sent,
* thus bypassing the need for the user to select an app.
*
* @param context the context
* @param isRecording are we currently recording?
* @param targetPackage the target package
* @param targetClass the target class
* @param trackIds the track ids
*/
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
if (trackIds.length == 0) {
return;
}
String trackIdList = ContentProviderUtils.formatIdListForUri(trackIds);
ArrayList<Uri> uris = new ArrayList<>();
uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_TRACKS_BY_IDS.getUri(), trackIdList));
uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getUri(), trackIdList));
uris.add(MARKERS_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getUri(), trackIdList));
Intent intent = new Intent(ACTION_DASHBOARD);
intent.putExtra(EXTRAS_PROTOCOL_VERSION, CURRENT_VERSION);
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn());
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen());
intent.putExtra(EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK, isRecording);
if (isRecording) {
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen());
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX));
clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX)));
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
intent.setClipData(clipData);
if (targetPackage != null && targetClass != null) {
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
intent.setClassName(targetPackage, targetClass);
} else {
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
}
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Dashboard not installed; cannot start it.");
Toast.makeText(context, R.string.show_on_dashboard_not_installed, Toast.LENGTH_SHORT).show();
}
}
}
@@ -10,7 +10,6 @@ import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackBuilder; import de.dennisguse.opentracks.data.models.TrackBuilder;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
public class StartRecording extends AbstractAPIActivity { public class StartRecording extends AbstractAPIActivity {
@@ -61,7 +60,7 @@ public class StartRecording extends AbstractAPIActivity {
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null); String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null); String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
if (targetClass != null && targetPackage != null) { if (targetClass != null && targetPackage != null) {
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId); DataProvider.startDashboard(this, true, targetPackage, targetClass, trackId);
} }
} }
@@ -185,14 +185,15 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
} }
private void startRecording() { private void startRecording() {
// Update instance variables
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
startSensors(); startSensors();
voiceAnnouncementManager.start(trackRecordingManager.getTrackStatistics()); voiceAnnouncementManager.start(trackRecordingManager.getTrackStatistics());
notificationManager.onRecordingStarted(this, recordingStatus.trackId()); notificationManager.onRecordingStarted(this, recordingStatus.trackId());
// Update instance variables
updateRecordingData.run();
handler.postDelayed(updateRecordingData, RECORDING_DATA_UPDATE_INTERVAL.toMillis());
} }
public void tryStartSensors() { public void tryStartSensors() {
@@ -778,7 +778,14 @@ public class PreferencesUtils {
// We want to really clear settings now. // We want to really clear settings now.
sharedPreferences.edit().clear().commit(); sharedPreferences.edit().clear().commit();
} }
// Each setting file must be mentioned here.
PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain); PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain);
PreferenceManager.setDefaultValues(context, R.xml.settings_defaults, readAgain);
PreferenceManager.setDefaultValues(context, R.xml.settings_user_interface, readAgain);
PreferenceManager.setDefaultValues(context, R.xml.settings_sensors, readAgain);
PreferenceManager.setDefaultValues(context, R.xml.settings_import_export, readAgain);
PreferenceManager.setDefaultValues(context, R.xml.settings_public_api, readAgain);
} }
public static Uri getDefaultExportDirectoryUri() { public static Uri getDefaultExportDirectoryUri() {
@@ -1,127 +0,0 @@
package de.dennisguse.opentracks.util;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Create an {@link Intent} to request showing tracks on a Map or a Dashboard.
* The receiving {@link android.app.Activity} gets temporary access to the {@link TracksColumns} and the {@link TrackPointsColumns} (incl. update).
*/
//TODO Add a test for the table structure exposed via the DashboardAPI.
public class IntentDashboardUtils {
private static final String TAG = IntentDashboardUtils.class.getSimpleName();
private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
/**
* Assume "v1" if not present.
*/
private static final String EXTRAS_PROTOCOL_VERSION = "PROTOCOL_VERSION";
/**
* version 1: the initial version.
* version 2: replaced pause/resume trackpoints for track segmentation (lat=100 / lat=200) by TrackPoint.Type.
* version 3: renamed track database columns.
*/
private static final int CURRENT_VERSION = 3;
private static final String EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK = "EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK";
private static final String EXTRAS_SHOULD_KEEP_SCREEN_ON = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_WHEN_LOCKED = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_FULLSCREEN = "EXTRAS_SHOULD_FULLSCREEN";
private static final int TRACK_URI_INDEX = 0;
private static final int TRACKPOINTS_URI_INDEX = 1;
private static final int MARKERS_URI_INDEX = 2;
private static final int NONE_SELECTED = -1;
private IntentDashboardUtils() {
}
/**
* Send intent to show tracks on a map (needs an another app) as resource URIs.
*
* @param context the context
* @param isRecording are we currently recording?
* @param trackIds the track ids
*/
public static void showTrackOnMap(Context context, boolean isRecording, Track.Id... trackIds) {
startDashboard(context, isRecording, null, null, trackIds);
}
/**
* Send intent to show tracks on a map (needs an another app) as resource URIs.
* By providing a targetPackage and targetClass an explicit intent can be sent,
* thus bypassing the need for the user to select an app.
*
* @param context the context
* @param isRecording are we currently recording?
* @param targetPackage the target package
* @param targetClass the target class
* @param trackIds the track ids
*/
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
if (trackIds.length == 0) {
return;
}
String trackIdList = ContentProviderUtils.formatIdListForUri(trackIds);
ArrayList<Uri> uris = new ArrayList<>();
uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(TracksColumns.CONTENT_URI, trackIdList));
uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(TrackPointsColumns.CONTENT_URI_BY_TRACKID, trackIdList));
uris.add(MARKERS_URI_INDEX, Uri.withAppendedPath(MarkerColumns.CONTENT_URI_BY_TRACKID, trackIdList));
Intent intent = new Intent(ACTION_DASHBOARD);
intent.putExtra(EXTRAS_PROTOCOL_VERSION, CURRENT_VERSION);
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn());
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen());
intent.putExtra(EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK, isRecording);
if (isRecording) {
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen());
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX));
clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX)));
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
intent.setClipData(clipData);
if (targetPackage != null && targetClass != null) {
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
intent.setClassName(targetPackage, targetClass);
} else {
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
}
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Dashboard not installed; cannot start it.");
Toast.makeText(context, R.string.show_on_dashboard_not_installed, Toast.LENGTH_SHORT).show();
}
}
}
+32 -9
View File
@@ -18,7 +18,7 @@ limitations under the License.
<string name="activity_type_airplane">αεροπλάνο</string> <string name="activity_type_airplane">αεροπλάνο</string>
<string name="activity_type_atv">ATV</string> <string name="activity_type_atv">ATV</string>
<string name="activity_type_biking">ποδηλασία</string> <string name="activity_type_biking">ποδηλασία</string>
<string name="activity_type_blimp">μικρό αερόστατο</string> <string name="activity_type_blimp">αερόπλοιο</string>
<string name="activity_type_boat">πλοίο</string> <string name="activity_type_boat">πλοίο</string>
<string name="activity_type_climbing">αναρρίχηση</string> <string name="activity_type_climbing">αναρρίχηση</string>
<string name="activity_type_commercial_airplane">εμπορικό αεροπλάνο</string> <string name="activity_type_commercial_airplane">εμπορικό αεροπλάνο</string>
@@ -29,8 +29,8 @@ limitations under the License.
<string name="activity_type_driving">οδήγηση</string> <string name="activity_type_driving">οδήγηση</string>
<string name="activity_type_driving_bus">οδήγηση λεωφορείου</string> <string name="activity_type_driving_bus">οδήγηση λεωφορείου</string>
<string name="activity_type_driving_car">οδήγηση αυτοκινήτου</string> <string name="activity_type_driving_car">οδήγηση αυτοκινήτου</string>
<string name="activity_type_ferry">Φέρι μποτ</string> <string name="activity_type_ferry">φέρι μποτ</string>
<string name="activity_type_frisbee">φρισμπι</string> <string name="activity_type_frisbee">φρίσμπι</string>
<string name="activity_type_gliding">ανεμόπτερο</string> <string name="activity_type_gliding">ανεμόπτερο</string>
<string name="activity_type_hang_gliding">αιωροπτερισμός</string> <string name="activity_type_hang_gliding">αιωροπτερισμός</string>
<string name="activity_type_helicopter">ελικόπτερο</string> <string name="activity_type_helicopter">ελικόπτερο</string>
@@ -44,15 +44,15 @@ limitations under the License.
<string name="activity_type_land_sailing">ιστιοπλοΐα γης</string> <string name="activity_type_land_sailing">ιστιοπλοΐα γης</string>
<string name="activity_type_mixed_type">μικτού τύπου</string> <string name="activity_type_mixed_type">μικτού τύπου</string>
<string name="activity_type_motor_bike">μηχανάκι</string> <string name="activity_type_motor_bike">μηχανάκι</string>
<string name="activity_type_motor_boating">Μηχανοκίνητα σκάφη</string> <string name="activity_type_motor_boating">μηχανοκίνητο σκάφος</string>
<string name="activity_type_mountain_biking">Ποδηλασία στο βουνό</string> <string name="activity_type_mountain_biking">ποδηλασία στο βουνό</string>
<string name="activity_type_off_trail_hiking">πεζοπορία σε ανώμαλο έδαφος</string> <string name="activity_type_off_trail_hiking">πεζοπορία σε ανώμαλο έδαφος</string>
<string name="activity_type_other">Άλλη</string> <string name="activity_type_other">άλλη</string>
<string name="activity_type_paddling">κωπηλασία</string> <string name="activity_type_paddling">κωπηλασία</string>
<string name="activity_type_para_gliding">παραπέντε</string> <string name="activity_type_para_gliding">παραπέντε</string>
<string name="activity_type_rc_airplane">τηλεχειριζόμενο αεροπλάνο</string> <string name="activity_type_rc_airplane">τηλεχειριζόμενο αεροπλάνο</string>
<string name="activity_type_rc_boat">Τηλεχειριζόμενη βάρκα</string> <string name="activity_type_rc_boat">τηλεχειριζόμενη βάρκα</string>
<string name="activity_type_rc_helicopter">Τηλεχειριζόμενο ελικόπτερο</string> <string name="activity_type_rc_helicopter">τηλεχειριζόμενο ελικόπτερο</string>
<string name="activity_type_riding">ιππασία</string> <string name="activity_type_riding">ιππασία</string>
<string name="activity_type_road_biking">ποδηλασία</string> <string name="activity_type_road_biking">ποδηλασία</string>
<string name="activity_type_roller_skiing">ρόλερ σκι ρολό</string> <string name="activity_type_roller_skiing">ρόλερ σκι ρολό</string>
@@ -302,4 +302,27 @@ limitations under the License.
<string name="waypoint_type_waterfall">καταρράκτης</string> <string name="waypoint_type_waterfall">καταρράκτης</string>
<string name="waypoint_type_water_fountain">συντριβάνι</string> <string name="waypoint_type_water_fountain">συντριβάνι</string>
<string name="waypoint_type_waypoint">σημείο διαδρομής</string> <string name="waypoint_type_waypoint">σημείο διαδρομής</string>
</resources> <string name="about_preference_title">Σχετικά</string>
<string name="about_url">Ιστότοπους εγχειρήματος: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Όνομα έκδοσης: %1$s</string>
<string name="about_version_code">Κωδικός έκδοσης: %1$d</string>
<string name="about_third_party_data">Δεδομένα από τρίτους</string>
<string name="about_libraries">Βιβλιοθήκες</string>
<string name="help_feature_title">Το OpenTracks έχει τα ακόλουθα χαρακτηριστικά:</string>
<string name="help_data_google_play_services_title">Το OpenTracks χρειάζεται/χρησιμοποιεί τις Υπηρεσίες Google Play;</string>
<string name="help_data_google_play_services_content">Όχι</string>
<string name="help_recording_title">Πώς το OpenTracks καταγράφει τα δεδομένα;</string>
<string name="help_track_map_title">Πώς να εμφανιστεί ένα ίχνος στον χάρτη;</string>
<string name="help_gps_troubleshooting_title">Το OpenTracks δεν καταγράφει τοποθεσίες;</string>
<string name="help_support_title">Πώς να υποστηρίξω το OpenTracks;</string>
<string name="open_track_as_trackfileformat">Ανοίγμα ως %1$s</string>
<string name="activity_type_escooter">ηλεκτρικό πατίνι</string>
<string name="activity_type_kickscooter">πατίνι</string>
<string name="power_x_watt">Ισχύς {x, plural, =1 {1 watt} other {# watts} }</string>
<string name="power">Ισχύς</string>
<string name="report_bug">Αναφορά προβλήματος</string>
<string name="always">Πάντα</string>
<string name="settings_recording_min_sampling_interval_title">Χρονικό μεσοδιάστημα δειγματοληψίας</string>
<string name="settings_recording_location_distance_summary">%1$s μεταξύ καταγεγραμμένων τοποθεσιών</string>
<string name="field_remove_from_layout">Αφαίρεση</string>
</resources>