diff --git a/README.md b/README.md index 812b20302..0310c367a 100644 --- a/README.md +++ b/README.md @@ -118,27 +118,33 @@ __Only required permission:__ * _ACCESS_FINE_LOCATION_: required to use the GPS. ### API -The following functionalities by sending from an external application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)). -The API can be invoked by sending an Intent to start an activity. -Package (depends on the variant installed): + +OpenTracks includes an API for starting/stopping recording by another installed application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)). +The API can be invoked by sending an explicit Intent to start an activity. + +`Package` (depends on the variant installed): * F-Droid: `de.dennisguse.opentracks` * GooglePlay: `de.dennisguse.opentracks.playStore` * Debug: `de.dennisguse.opentracks.debug` * Nightly: `de.dennisguse.opentracks.nightly` -Activity: +`Class`: * **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording` -* For testing via adb: `adb shell am start -n "package/activity"` +For testing via adb: `adb shell am start -n "package/class"` + +The Start/Stop API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings. ## Custom Dashboards (incl. map) -As of v3.3.1, OpenTracks supports custom dashboards. -The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). -The developer documentation is in [README_API.md](README_API.md). +As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live tracks. + +The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track statistics on supported wearables. Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)). However, this is rather slow and does not provide updates while recording. +The developer documentation is in [README_API.md](README_API.md). + ## Project history _OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)). diff --git a/README_API.md b/README_API.md index 8ebc58d1d..55ea61f77 100644 --- a/README_API.md +++ b/README_API.md @@ -4,11 +4,13 @@ OpenTracks by itself only provides basic features for data visualization and also lacks functionality for analytics. By itself OpenTracks provides a basic dashboard that can show summary of a track (while recording as well as afterwards). Although this might be (mostly) sufficient, it has some limitations. -This include limited configurability (e.g., ordering of shown elements) and the lack of Internet access. -In fact, the later is considered a feature but severely limits the implementation presenting a map (at least in an easy way). +This includes limited configurability (e.g., ordering of shown elements) and the lack of internet access. +In fact, the latter is considered a feature but severely limits the implementation presenting a map (at least in an easy way). For the time being, the OpenTracks Data API can be used to implement mainly custom dashboards. +The Data API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings. + ## Implementation OpenTracks by default does not expose any data to 3rd-party applications. On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary), the `TrackPoint`s, and the `Waypoint`s of one or more selected tracks. @@ -17,11 +19,16 @@ These URIs notify observers if on content changes (e.g., a new `TrackPoint` was Limitations: * only tracks selected by the user are exposed, -* `Waypoints` are not exposed, +* `Waypoint`s are not exposed, * access is only granted temporarily and automatically revoked (see `FLAG_GRANT_READ_URI_PERMISSION`), -* no write access is possible, and -* neither track recordings can be started, paused, or stopped. +* no write access is possible + +The Data API can also be started automatically by leveraging Extra fields in the explicit Intent for the Start/Stop API. +An application that's starting a recording with an explicit Intent can add the `STATS_TARGET_PACKAGE` and `STATS_TARGET_CLASS` Extra fields to let OpenTracks know it wants to receive the Data API URIs. +OpenTracks will then (if enabled in the settings) automatically return an explicit Intent to the Package + Class as defined in the received Extra fields, including the Data API URIs and read permissions. + +By using explicit Intents for the calls and URIs with specific temporary read permissions, OpenTracks tries to protect against data leaking as much as possible. All required information can be found in `IntentDashboardUtils.java`. -A _reference implementation_ is the [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard). \ No newline at end of file +A _reference implementation_ is the [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard). diff --git a/src/main/java/de/dennisguse/opentracks/publicapi/StartRecording.java b/src/main/java/de/dennisguse/opentracks/publicapi/StartRecording.java index c6372919e..9a7c7471b 100644 --- a/src/main/java/de/dennisguse/opentracks/publicapi/StartRecording.java +++ b/src/main/java/de/dennisguse/opentracks/publicapi/StartRecording.java @@ -1,11 +1,25 @@ package de.dennisguse.opentracks.publicapi; +import android.os.Bundle; + +import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.services.TrackRecordingService; +import de.dennisguse.opentracks.settings.PreferencesUtils; +import de.dennisguse.opentracks.util.IntentDashboardUtils; public class StartRecording extends AbstractAPIActivity { + private static final String TAG = StartRecording.class.getSimpleName(); protected void execute(TrackRecordingService service) { - service.startNewTrack(); + Track.Id trackId = service.startNewTrack(); + if ((trackId != null) && (PreferencesUtils.isPublicAPIDashboardEnabled())) { + Bundle bundle = getIntent().getExtras(); + if (bundle != null) { + String targetPackage = bundle.getString("STATS_TARGET_PACKAGE", null); + String targetClass = bundle.getString("STATS_TARGET_CLASS", null); + IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId); + } + } } @Override diff --git a/src/main/java/de/dennisguse/opentracks/settings/MainSettingsFragment.java b/src/main/java/de/dennisguse/opentracks/settings/MainSettingsFragment.java index 8227fdb59..a2ff31fe9 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/MainSettingsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/settings/MainSettingsFragment.java @@ -57,6 +57,11 @@ public class MainSettingsFragment extends PreferenceFragmentCompat { return true; }); + findPreference(getString(R.string.settings_api_key)).setOnPreferenceClickListener(preference -> { + ((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_api_key)); + return true; + }); + findPreference(getString(R.string.settings_open_tracks_key)).setOnPreferenceClickListener(preference -> { ((SettingsActivity) getActivity()).openScreen(getString(R.string.settings_open_tracks_key)); return true; diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 61893dc92..5a73e7cdb 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -182,6 +182,10 @@ public class PreferencesUtils { return getBoolean(R.string.publicapi_enabled_key, resources.getBoolean(R.bool.publicapi_enabled_default)); } + public static boolean isPublicAPIDashboardEnabled() { + return getBoolean(R.string.publicapi_dashboard_enabled_key, resources.getBoolean(R.bool.publicapi_dashboard_enabled_default)); + } + public static boolean isMetricUnits() { final String STATS_UNIT = resources.getString(R.string.stats_units_default); return STATS_UNIT.equals(getString(R.string.stats_units_key, STATS_UNIT)); diff --git a/src/main/java/de/dennisguse/opentracks/settings/PublicAPISettingsFragment.java b/src/main/java/de/dennisguse/opentracks/settings/PublicAPISettingsFragment.java new file mode 100644 index 000000000..80e0cb386 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/PublicAPISettingsFragment.java @@ -0,0 +1,21 @@ +package de.dennisguse.opentracks.settings; + +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; + +import de.dennisguse.opentracks.R; + +public class PublicAPISettingsFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.settings_public_api); + } + + @Override + public void onStart() { + super.onStart(); + ((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_api_title); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java index 68068e0a0..a0ca1bc1e 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java @@ -84,6 +84,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity fragment = new AnnouncementsSettingsFragment(); } else if (key.equals(getString(R.string.settings_import_export_key))) { fragment = new ImportExportSettingsFragment(); + } else if (key.equals(getString(R.string.settings_api_key))) { + fragment = new PublicAPISettingsFragment(); } else if (key.equals(getString(R.string.settings_open_tracks_key))) { fragment = new OpenTracksSettingsFragment(); } diff --git a/src/main/java/de/dennisguse/opentracks/util/IntentDashboardUtils.java b/src/main/java/de/dennisguse/opentracks/util/IntentDashboardUtils.java index 53b1a9e88..42ebcdb07 100644 --- a/src/main/java/de/dennisguse/opentracks/util/IntentDashboardUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/IntentDashboardUtils.java @@ -4,6 +4,7 @@ import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.util.Log; import androidx.annotation.NonNull; @@ -23,6 +24,7 @@ import de.dennisguse.opentracks.settings.PreferencesUtils; * The receiving {@link android.app.Activity} gets temporary access to the {@link TracksColumns} and the {@link TrackPointsColumns} (incl. update). */ public class IntentDashboardUtils { + private static final String TAG = IntentDashboardUtils.class.getSimpleName(); private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard"; @@ -58,6 +60,15 @@ public class IntentDashboardUtils { * @param trackIds the track ids */ public static void startDashboard(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. + */ + public static void startDashboard(Context context, boolean isRecording, String targetPackage, String targetClass, Track.Id... trackIds) { if (trackIds.length == 0) { return; } @@ -87,6 +98,13 @@ public class IntentDashboardUtils { 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 + ")"); + } + context.startActivity(intent); } diff --git a/src/main/res/drawable/ic_baseline_api_24.xml b/src/main/res/drawable/ic_baseline_api_24.xml new file mode 100644 index 000000000..da2cacb3e --- /dev/null +++ b/src/main/res/drawable/ic_baseline_api_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index aa215139b..272d28382 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -291,8 +291,11 @@ preventReimportTrackKey true + settingsAPI apiEnabledKey false + apiDashboardEnabledKey + false 1500 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 5fb9c923b..15432b622 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -447,10 +447,14 @@ limitations under the License. Preferred units + Public API Public API Other installed applications can start or stop recordings. Only OpenTracks can start and stop recordings. OpenTracks Public API is disabled: it can be enabled in the settings. + Automatic data transfer + An application that started a recording can also access the recorded data. + Recorded data will not be shared automatically with other applications. Import diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index 17ee9db66..2889ec0f3 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -50,12 +50,11 @@ android:summary="@string/settings_reset_summary" android:title="@string/settings_all_reset" /> - + + + + + + + + + \ No newline at end of file