From ad450865c55d5bdef6b44581187f04910aea14a2 Mon Sep 17 00:00:00 2001 From: kavikhalique Date: Sat, 31 Aug 2024 09:04:47 +0100 Subject: [PATCH] [android] Implement the track recorder Signed-off-by: kavikhalique Co-authored-by: Roman Tsisyk Signed-off-by: Roman Tsisyk --- .../cpp/app/organicmaps/TrackRecorder.cpp | 28 +++++++-- .../java/app/organicmaps/MwmActivity.java | 44 +++++++++++--- .../java/app/organicmaps/MwmApplication.java | 2 +- .../organicmaps/location/LocationHelper.java | 2 +- .../organicmaps/location/TrackRecorder.java | 15 +++-- .../location/TrackRecordingService.java | 53 ++++++++++++----- .../maplayer/MapButtonsController.java | 3 +- .../maplayer/MapButtonsViewModel.java | 2 +- .../routing/NavigationService.java | 2 +- .../util/bottomsheet/MenuAdapter.java | 6 +- ...ath_off.xml => ic_track_recording_off.xml} | 0 ..._path_on.xml => ic_track_recording_on.xml} | 0 data/strings/strings.txt | 57 ++----------------- 13 files changed, 121 insertions(+), 93 deletions(-) rename android/app/src/main/res/drawable/{ic_trace_path_off.xml => ic_track_recording_off.xml} (100%) rename android/app/src/main/res/drawable/{ic_trace_path_on.xml => ic_track_recording_on.xml} (100%) diff --git a/android/app/src/main/cpp/app/organicmaps/TrackRecorder.cpp b/android/app/src/main/cpp/app/organicmaps/TrackRecorder.cpp index 7cf0dfed6a..05b7f5cd06 100644 --- a/android/app/src/main/cpp/app/organicmaps/TrackRecorder.cpp +++ b/android/app/src/main/cpp/app/organicmaps/TrackRecorder.cpp @@ -26,14 +26,32 @@ extern "C" } JNIEXPORT void JNICALL - Java_app_organicmaps_location_TrackRecorder_nativeSetDuration(JNIEnv * env, jclass clazz, jint durationHours) + Java_app_organicmaps_location_TrackRecorder_nativeStartTrackRecording(JNIEnv * env, jclass clazz) { - GpsTracker::Instance().SetDuration(std::chrono::hours(durationHours)); + frm()->StartTrackRecording(); } - JNIEXPORT jint JNICALL - Java_app_organicmaps_location_TrackRecorder_nativeGetDuration(JNIEnv * env, jclass clazz) + JNIEXPORT void JNICALL + Java_app_organicmaps_location_TrackRecorder_nativeStopTrackRecording(JNIEnv * env, jclass clazz) { - return static_cast(GpsTracker::Instance().GetDuration().count()); + frm()->StopTrackRecording(); + } + + JNIEXPORT void JNICALL + Java_app_organicmaps_location_TrackRecorder_nativeSaveTrackRecordingWithName(JNIEnv * env, jclass clazz, jstring name) + { + frm()->SaveTrackRecordingWithName(jni::ToNativeString(env, name)); + } + + JNIEXPORT jboolean JNICALL + Java_app_organicmaps_location_TrackRecorder_nativeIsTrackRecordingEmpty(JNIEnv * env, jclass clazz) + { + return frm()->IsTrackRecordingEmpty(); + } + + JNIEXPORT jboolean JNICALL + Java_app_organicmaps_location_TrackRecorder_nativeIsTrackRecordingEnabled(JNIEnv * env, jclass clazz) + { + return frm()->IsTrackRecordingEnabled(); } } diff --git a/android/app/src/main/java/app/organicmaps/MwmActivity.java b/android/app/src/main/java/app/organicmaps/MwmActivity.java index 17f23ff99a..32ed7fc03e 100644 --- a/android/app/src/main/java/app/organicmaps/MwmActivity.java +++ b/android/app/src/main/java/app/organicmaps/MwmActivity.java @@ -106,6 +106,7 @@ import app.organicmaps.util.Utils; import app.organicmaps.util.bottomsheet.MenuBottomSheetFragment; import app.organicmaps.util.bottomsheet.MenuBottomSheetItem; import app.organicmaps.util.log.Logger; +import app.organicmaps.widget.StackedButtonsDialog; import app.organicmaps.widget.menu.MainMenu; import app.organicmaps.widget.placepage.PlacePageController; import app.organicmaps.widget.placepage.PlacePageData; @@ -278,11 +279,8 @@ public class MwmActivity extends BaseMwmFragmentActivity // This is for the case when track recording was enabled but due to any reasons // App crashed so we need the restart or stop the whole service again properly // by checking all the necessary permissions - if (TrackRecorder.nativeIsEnabled()) - { - TrackRecorder.nativeSetEnabled(false); + if (TrackRecorder.nativeIsTrackRecordingEnabled()) startTrackRecording(false); - } processIntent(); migrateOAuthCredentials(); @@ -2290,19 +2288,49 @@ public class MwmActivity extends BaseMwmFragmentActivity private void stopTrackRecording() { - //Toast.makeText(this, R.string.track_recording_disabled, Toast.LENGTH_SHORT).show(); TrackRecordingService.stopService(getApplicationContext()); mMapButtonsViewModel.setTrackRecorderState(false); } private void onTrackRecordingOptionSelected() { - if (TrackRecorder.nativeIsEnabled()) - stopTrackRecording(); + if (TrackRecorder.nativeIsTrackRecordingEnabled()) + showTrackSaveDialog(); else startTrackRecording(true); } + private void showTrackSaveDialog() + { + if (TrackRecorder.nativeIsTrackRecordingEmpty()) + { + Toast.makeText(this, R.string.track_recording_toast_nothing_to_save, Toast.LENGTH_SHORT) + .show(); + stopTrackRecording(); + return; + } + + dismissAlertDialog(); + mAlertDialog = new StackedButtonsDialog.Builder(this) + .setTitle(R.string.track_recording_alert_title) + .setCancelable(false) + // Negative/Positive/Neutral doesn't do not have the usual meaning here. + .setPositiveButton(R.string.continue_recording, (dialog, which) -> { + mAlertDialog = null; + }) + .setNeutralButton(R.string.stop_without_saving, (dialog, which) -> { + stopTrackRecording(); + mAlertDialog = null; + }) + .setNegativeButton(R.string.save, (dialog, which) -> { + TrackRecorder.nativeSaveTrackRecordingWithName(""); + stopTrackRecording(); + mAlertDialog = null; + }) + .build(); + mAlertDialog.show(); + } + public void onShareLocationOptionSelected() { closeFloatingPanels(); @@ -2327,7 +2355,7 @@ public class MwmActivity extends BaseMwmFragmentActivity if (!TextUtils.isEmpty(mDonatesUrl)) items.add(new MenuBottomSheetItem(R.string.donate, R.drawable.ic_donate, this::onDonateOptionSelected)); items.add(new MenuBottomSheetItem(R.string.settings, R.drawable.ic_settings, this::onSettingsOptionSelected)); - items.add(new MenuBottomSheetItem(R.string.recent_track, R.drawable.ic_trace_path_off, -1, this::onTrackRecordingOptionSelected)); + items.add(new MenuBottomSheetItem(R.string.start_track_recording, R.drawable.ic_track_recording_off, -1, this::onTrackRecordingOptionSelected)); items.add(new MenuBottomSheetItem(R.string.share_my_location, R.drawable.ic_share, this::onShareLocationOptionSelected)); return items; } diff --git a/android/app/src/main/java/app/organicmaps/MwmApplication.java b/android/app/src/main/java/app/organicmaps/MwmApplication.java index 8393cb4911..4951e5936a 100644 --- a/android/app/src/main/java/app/organicmaps/MwmApplication.java +++ b/android/app/src/main/java/app/organicmaps/MwmApplication.java @@ -371,7 +371,7 @@ public class MwmApplication extends Application implements Application.ActivityL Logger.i(LOCATION_TAG, "Navigation is in progress, keeping location in the background"); else if (!Map.isEngineCreated() || LocationState.getMode() == LocationState.PENDING_POSITION) Logger.i(LOCATION_TAG, "PENDING_POSITION mode, keeping location in the background"); - else if (TrackRecorder.nativeIsEnabled()) + else if (TrackRecorder.nativeIsTrackRecordingEnabled()) Logger.i(LOCATION_TAG, "Track Recordr is active, keeping location in the background"); else { diff --git a/android/app/src/main/java/app/organicmaps/location/LocationHelper.java b/android/app/src/main/java/app/organicmaps/location/LocationHelper.java index a40299b326..b670a5434d 100644 --- a/android/app/src/main/java/app/organicmaps/location/LocationHelper.java +++ b/android/app/src/main/java/app/organicmaps/location/LocationHelper.java @@ -294,7 +294,7 @@ public class LocationHelper implements BaseLocationProvider.Listener if (RoutingController.get().isNavigating()) return INTERVAL_NAVIGATION_MS; - if (TrackRecorder.nativeIsEnabled()) + if (TrackRecorder.nativeIsTrackRecordingEnabled()) return INTERVAL_TRACK_RECORDING; final int mode = Map.isEngineCreated() ? LocationState.getMode() : LocationState.NOT_FOLLOW_NO_POSITION; diff --git a/android/app/src/main/java/app/organicmaps/location/TrackRecorder.java b/android/app/src/main/java/app/organicmaps/location/TrackRecorder.java index e86e350f64..a20847ca80 100644 --- a/android/app/src/main/java/app/organicmaps/location/TrackRecorder.java +++ b/android/app/src/main/java/app/organicmaps/location/TrackRecorder.java @@ -1,11 +1,14 @@ package app.organicmaps.location; -import app.organicmaps.util.Listeners; - public class TrackRecorder { - public static native void nativeSetEnabled(boolean enable); - public static native boolean nativeIsEnabled(); - public static native void nativeSetDuration(int hours); - public static native int nativeGetDuration(); + public static native void nativeStartTrackRecording(); + + public static native void nativeStopTrackRecording(); + + public static native void nativeSaveTrackRecordingWithName(String name); + + public static native boolean nativeIsTrackRecordingEmpty(); + + public static native boolean nativeIsTrackRecordingEnabled(); } diff --git a/android/app/src/main/java/app/organicmaps/location/TrackRecordingService.java b/android/app/src/main/java/app/organicmaps/location/TrackRecordingService.java index e57270c77c..61deba14d6 100644 --- a/android/app/src/main/java/app/organicmaps/location/TrackRecordingService.java +++ b/android/app/src/main/java/app/organicmaps/location/TrackRecordingService.java @@ -1,18 +1,15 @@ package app.organicmaps.location; -import android.Manifest; import android.app.ForegroundServiceStartNotAllowedException; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.location.Location; import android.os.Build; import android.os.Handler; import android.os.IBinder; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -35,6 +32,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; public class TrackRecordingService extends Service implements LocationListener { public static final String TRACK_REC_CHANNEL_ID = "TRACK RECORDING"; + private static final String STOP_TRACK_RECORDING = "STOP_TRACK_RECORDING"; public static final int TRACK_REC_NOTIFICATION_ID = 54321; private NotificationCompat.Builder mNotificationBuilder; private static final String TAG = TrackRecordingService.class.getSimpleName(); @@ -44,6 +42,7 @@ public class TrackRecordingService extends Service implements LocationListener private boolean mWarningNotification = false; private NotificationCompat.Builder mWarningBuilder; private PendingIntent mPendingIntent; + private PendingIntent mExitPendingIntent; @Nullable @Override @@ -55,10 +54,8 @@ public class TrackRecordingService extends Service implements LocationListener @RequiresPermission(value = ACCESS_FINE_LOCATION) public static void startForegroundService(@NonNull Context context) { - if (TrackRecorder.nativeGetDuration() != 24) - TrackRecorder.nativeSetDuration(24); - if (!TrackRecorder.nativeIsEnabled()) - TrackRecorder.nativeSetEnabled(true); + if (!TrackRecorder.nativeIsTrackRecordingEnabled()) + TrackRecorder.nativeStartTrackRecording(); LocationHelper.from(context).restartWithNewMode(); ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class)); } @@ -68,7 +65,7 @@ public class TrackRecordingService extends Service implements LocationListener final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); final NotificationChannelCompat channel = new NotificationChannelCompat.Builder(TRACK_REC_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) - .setName(context.getString(R.string.recent_track)) + .setName(context.getString(R.string.track_recording)) .setLightsEnabled(false) .setVibrationEnabled(false) .build(); @@ -83,10 +80,23 @@ public class TrackRecordingService extends Service implements LocationListener final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? 0 : PendingIntent.FLAG_IMMUTABLE; final Intent contentIntent = new Intent(context, MwmActivity.class); mPendingIntent = PendingIntent.getActivity(context, 0, contentIntent, - PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); + PendingIntent.FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE); return mPendingIntent; } + private PendingIntent getExitPendingIntent(@NonNull Context context) + { + if (mExitPendingIntent != null) + return mExitPendingIntent; + + final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? 0 : PendingIntent.FLAG_IMMUTABLE; + final Intent exitIntent = new Intent(context, TrackRecordingService.class); + exitIntent.setAction(STOP_TRACK_RECORDING); + mExitPendingIntent = PendingIntent.getService(context, 1, exitIntent, + PendingIntent.FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE); + return mExitPendingIntent; + } + @NonNull public NotificationCompat.Builder getNotificationBuilder(@NonNull Context context) { @@ -101,8 +111,8 @@ public class TrackRecordingService extends Service implements LocationListener .setShowWhen(true) .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.ic_splash) - .setContentTitle(context.getString(R.string.recent_track)) - .setContentText(context.getString(R.string.track_recording)) + .setContentTitle(context.getString(R.string.track_recording)) + .addAction(0, context.getString(R.string.navigation_stop_button), getExitPendingIntent(context)) .setContentIntent(getPendingIntent(context)) .setColor(ContextCompat.getColor(context, R.color.notification)); @@ -120,8 +130,8 @@ public class TrackRecordingService extends Service implements LocationListener { mNotificationBuilder = null; mWarningBuilder = null; - if (TrackRecorder.nativeIsEnabled()) - TrackRecorder.nativeSetEnabled(false); + if (TrackRecorder.nativeIsTrackRecordingEnabled()) + TrackRecorder.nativeStopTrackRecording(); mHandler.removeCallbacks(mLocationTimeoutRunnable); LocationHelper.from(this).removeListener(this); // The notification is cancelled automatically by the system. @@ -130,6 +140,20 @@ public class TrackRecordingService extends Service implements LocationListener @Override public int onStartCommand(@NonNull Intent intent, int flags, int startId) { + final String action = intent.getAction(); + if (action != null && action.equals(STOP_TRACK_RECORDING)) + { + if (TrackRecorder.nativeIsTrackRecordingEnabled()) + { + if (!TrackRecorder.nativeIsTrackRecordingEmpty()) + TrackRecorder.nativeSaveTrackRecordingWithName(""); + else + TrackRecorder.nativeStopTrackRecording(); + } + stopSelf(); + return START_NOT_STICKY; + } + if (!MwmApplication.from(this).arePlatformAndCoreInitialized()) { Logger.w(TAG, "Application is not initialized"); @@ -146,7 +170,7 @@ public class TrackRecordingService extends Service implements LocationListener return START_NOT_STICKY; // The service will be stopped by stopSelf(). } - if (!TrackRecorder.nativeIsEnabled()) + if (!TrackRecorder.nativeIsTrackRecordingEnabled()) { Logger.i(TAG, "Service can't be started because Track Recorder is turned off in settings"); stopSelf(); @@ -201,6 +225,7 @@ public class TrackRecordingService extends Service implements LocationListener .setSmallIcon(R.drawable.warning_icon) .setContentTitle(context.getString(R.string.current_location_unknown_error_title)) .setContentText(context.getString(R.string.dialog_routing_location_turn_wifi)) + .addAction(0, context.getString(R.string.navigation_stop_button), getExitPendingIntent(context)) .setContentIntent(getPendingIntent(context)) .setColor(ContextCompat.getColor(context, R.color.notification_warning)); diff --git a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java index 3dadbd4ddc..adef1c30c0 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java @@ -30,7 +30,6 @@ import app.organicmaps.routing.RoutingController; import app.organicmaps.util.Config; import app.organicmaps.util.ThemeUtils; import app.organicmaps.util.UiUtils; -import app.organicmaps.util.log.Logger; import app.organicmaps.widget.menu.MyPositionButton; import app.organicmaps.widget.placepage.PlacePageViewModel; import com.google.android.material.badge.BadgeDrawable; @@ -243,7 +242,7 @@ public class MapButtonsController extends Fragment mBadgeDrawable.setVisible(count > 0); BadgeUtils.attachBadgeDrawable(mBadgeDrawable, menuButton); - updateMenuBadge(TrackRecorder.nativeIsEnabled()); + updateMenuBadge(TrackRecorder.nativeIsTrackRecordingEnabled()); } public void updateLayerButton() diff --git a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsViewModel.java b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsViewModel.java index 9ad0af1831..d4acfcb045 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsViewModel.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsViewModel.java @@ -12,7 +12,7 @@ public class MapButtonsViewModel extends ViewModel private final MutableLiveData mLayoutMode = new MutableLiveData<>(MapButtonsController.LayoutMode.regular); private final MutableLiveData mMyPositionMode = new MutableLiveData<>(); private final MutableLiveData mSearchOption = new MutableLiveData<>(); - private final MutableLiveData mTrackRecorderState = new MutableLiveData<>(TrackRecorder.nativeIsEnabled()); + private final MutableLiveData mTrackRecorderState = new MutableLiveData<>(TrackRecorder.nativeIsTrackRecordingEnabled()); public MutableLiveData getButtonsHidden() { diff --git a/android/app/src/main/java/app/organicmaps/routing/NavigationService.java b/android/app/src/main/java/app/organicmaps/routing/NavigationService.java index 247853cdd4..8e492dbbde 100644 --- a/android/app/src/main/java/app/organicmaps/routing/NavigationService.java +++ b/android/app/src/main/java/app/organicmaps/routing/NavigationService.java @@ -138,7 +138,7 @@ public class NavigationService extends Service implements LocationListener final int FLAG_IMMUTABLE = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? 0 : PendingIntent.FLAG_IMMUTABLE; final Intent contentIntent = new Intent(context, MwmActivity.class); final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, - PendingIntent.FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); + PendingIntent.FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE); final Intent exitIntent = new Intent(context, NavigationService.class); exitIntent.setAction(STOP_NAVIGATION); diff --git a/android/app/src/main/java/app/organicmaps/util/bottomsheet/MenuAdapter.java b/android/app/src/main/java/app/organicmaps/util/bottomsheet/MenuAdapter.java index d0e717f355..150be733fd 100644 --- a/android/app/src/main/java/app/organicmaps/util/bottomsheet/MenuAdapter.java +++ b/android/app/src/main/java/app/organicmaps/util/bottomsheet/MenuAdapter.java @@ -68,11 +68,11 @@ public class MenuAdapter extends RecyclerView.Adapter badge.setVisibility(View.GONE); } - if (item.iconRes == R.drawable.ic_trace_path_off && TrackRecorder.nativeIsEnabled()) + if (item.iconRes == R.drawable.ic_track_recording_off && TrackRecorder.nativeIsTrackRecordingEnabled()) { - iv.setImageResource(R.drawable.ic_trace_path_on); + iv.setImageResource(R.drawable.ic_track_recording_on); iv.setImageTintMode(null); - viewHolder.getTitleTextView().setText(R.string.recent_track); + viewHolder.getTitleTextView().setText(R.string.stop_track_recording); badge.setBackgroundResource(R.drawable.track_recorder_badge); badge.setVisibility(View.VISIBLE); } diff --git a/android/app/src/main/res/drawable/ic_trace_path_off.xml b/android/app/src/main/res/drawable/ic_track_recording_off.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_trace_path_off.xml rename to android/app/src/main/res/drawable/ic_track_recording_off.xml diff --git a/android/app/src/main/res/drawable/ic_trace_path_on.xml b/android/app/src/main/res/drawable/ic_track_recording_on.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_trace_path_on.xml rename to android/app/src/main/res/drawable/ic_track_recording_on.xml diff --git a/data/strings/strings.txt b/data/strings/strings.txt index a17b553859..45fd6e823a 100644 --- a/data/strings/strings.txt +++ b/data/strings/strings.txt @@ -6187,51 +6187,6 @@ zh-Hans = 其他 zh-Hant = 其他 - [recent_track] - comment = The name of the feature in the main menu, settings, and messages. - tags = android - en = Recent Track - af = Onlangse pad - ar = المسار الأخير - az = Son marşrut - be = Нядаўні маршрут - bg = Скорошна пътека - ca = Traces recents - cs = Historie polohy - da = Seneste sti - de = Letzter Track - el = Πρόσφατη διαδρομή - es = Trayecto reciente - et = Hiljutine rada - eu = Azken arrastoa - fa = مسیر اخیر - fi = Viimeisin reitti - fr = Parcours récent - he = מסלול אחרון - hi = हालिया ट्रैक - hu = Legutolsó útvonal - id = Jalur terkini - it = Percorso recente - ja = 最近のトラック - ko = 최근 추적 - lt = Pastaras takas - mr = अलीकडील ट्रॅक - nb = Siste rute - nl = Recente track - pl = Ostatnia trasa - pt = Percurso recente - pt-BR = Percurso recente - ro = Traseu recent - ru = Недавний путь - sk = Posledná trasa - sv = Senaste resväg - th = เส้นทางล่าสุด - tr = En sonki rotayı kaydet - uk = Недавній маршрут - vi = Tìm kiếm gần đây - zh-Hans = 最近的轨迹 - zh-Hant = 最近的軌跡 - [pref_map_auto_zoom] tags = android,ios en = Auto zoom @@ -31445,7 +31400,7 @@ [start_track_recording] comment = Prompt to start recording a track. - tags = ios + tags = android,ios en = Record Track af = Registreer roete ar = تسجيل المسار @@ -31491,7 +31446,7 @@ [stop_track_recording] comment = Prompt for stopping a track recording. - tags = ios + tags = android,ios en = Stop Track Recording af = Stop roete-opname ar = إيقاف تسجيل المسار @@ -31537,7 +31492,7 @@ [stop_without_saving] comment = Title for the "Stop Without Saving" action for the alert when saving a track recording. - tags = ios + tags = android,ios en = Stop Without Saving af = Stop sonder om te stoor ar = التوقف بدون حفظ @@ -31583,7 +31538,7 @@ [continue_recording] comment = Title for the "Stop Without Saving" action for the alert when saving a track recording. - tags = ios + tags = android,ios en = Continue Recording af = Voortgaan met Opname ar = متابعة التسجيل @@ -31629,7 +31584,7 @@ [track_recording_alert_title] comment = Title for the alert when saving a track recording. - tags = ios + tags = android,ios en = Save into Bookmarks and Tracks? af = Stoor na Boekmerke en paaie? ar = حفظ في الإشارات المرجعية والمسارات؟ @@ -31675,7 +31630,7 @@ [track_recording_toast_nothing_to_save] comment = Message for the toast when saving the track recording is finished but nothing to save. - tags = ios + tags = android,ios en = Track is empty - nothing to save af = Roete is leeg - niks om te red nie ar = المسار فارغ - لا يوجد شيء للحفظ