diff --git a/android/app/src/main/java/app/organicmaps/MwmActivity.java b/android/app/src/main/java/app/organicmaps/MwmActivity.java index c101917cb..87c0ba059 100644 --- a/android/app/src/main/java/app/organicmaps/MwmActivity.java +++ b/android/app/src/main/java/app/organicmaps/MwmActivity.java @@ -425,20 +425,61 @@ public class MwmActivity extends BaseMwmFragmentActivity private void shareMyLocation() { - final Location loc = MwmApplication.from(this).getLocationHelper().getSavedLocation(); - if (loc != null) + // Check if location sharing is already active + if (app.organicmaps.location.LocationSharingManager.getInstance().isSharing()) { - SharingUtils.shareLocation(this, loc); + // Stop sharing + app.organicmaps.location.LocationSharingManager.getInstance().stopSharing(); + mMapButtonsViewModel.setLocationSharingState(false); return; } - dismissLocationErrorDialog(); - mLocationErrorDialog = new MaterialAlertDialogBuilder(MwmActivity.this, R.style.MwmTheme_AlertDialog) - .setMessage(R.string.unknown_current_position) - .setCancelable(true) - .setPositiveButton(R.string.ok, null) - .setOnDismissListener(dialog -> mLocationErrorDialog = null) - .show(); + final Location loc = MwmApplication.from(this).getLocationHelper().getSavedLocation(); + if (loc == null) + { + dismissLocationErrorDialog(); + mLocationErrorDialog = new MaterialAlertDialogBuilder(MwmActivity.this, R.style.MwmTheme_AlertDialog) + .setMessage(R.string.unknown_current_position) + .setCancelable(true) + .setPositiveButton(R.string.ok, null) + .setOnDismissListener(dialog -> mLocationErrorDialog = null) + .show(); + return; + } + + // Show dialog with two options: share current coordinates or start live sharing + new MaterialAlertDialogBuilder(this, R.style.MwmTheme_AlertDialog) + .setTitle(R.string.share_my_location) + .setItems(new CharSequence[] { + getString(R.string.share_location_coordinates), + getString(R.string.share_location_live) + }, (dialog, which) -> { + if (which == 0) + { + // Share current coordinates + SharingUtils.shareLocation(this, loc); + } + else + { + // Start live location sharing + app.organicmaps.location.LocationSharingDialog.show(getSupportFragmentManager()); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + public void onLocationSharingStateChanged(boolean isSharing) + { + mMapButtonsViewModel.setLocationSharingState(isSharing); + MapButtonsController mapButtonsController = + (MapButtonsController) getSupportFragmentManager().findFragmentById(R.id.map_buttons); + if (mapButtonsController != null) + mapButtonsController.updateMenuBadge(); + + // Update share location button color in navigation menu + if (mNavigationController != null) + mNavigationController.refreshShareLocationColor(); } private void showDownloader(boolean openDownloaded) @@ -2496,7 +2537,10 @@ public class MwmActivity extends BaseMwmFragmentActivity 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, + final boolean isLocationSharingActive = app.organicmaps.location.LocationSharingManager.getInstance().isSharing(); + final int locationSharingTitleRes = isLocationSharingActive ? R.string.stop_sharing_my_location : R.string.share_my_location; + final int locationSharingBadge = isLocationSharingActive ? -1 : 0; + items.add(new MenuBottomSheetItem(locationSharingTitleRes, R.drawable.ic_share, locationSharingBadge, this::onShareLocationOptionSelected)); if (!BUTTON_HELP_CODE.equals(activeLeftButton)) diff --git a/android/app/src/main/java/app/organicmaps/location/LocationSharingDialog.java b/android/app/src/main/java/app/organicmaps/location/LocationSharingDialog.java index 379ef57c3..19fdb6cbd 100644 --- a/android/app/src/main/java/app/organicmaps/location/LocationSharingDialog.java +++ b/android/app/src/main/java/app/organicmaps/location/LocationSharingDialog.java @@ -147,6 +147,12 @@ public class LocationSharingDialog extends DialogFragment updateUI(); + // Notify the activity + if (getActivity() instanceof app.organicmaps.MwmActivity) + { + ((app.organicmaps.MwmActivity) getActivity()).onLocationSharingStateChanged(true); + } + // Auto-copy URL to clipboard copyUrlToClipboard(shareUrl); } @@ -167,6 +173,12 @@ public class LocationSharingDialog extends DialogFragment Toast.LENGTH_SHORT).show(); updateUI(); + + // Notify the activity + if (getActivity() instanceof app.organicmaps.MwmActivity) + { + ((app.organicmaps.MwmActivity) getActivity()).onLocationSharingStateChanged(false); + } } private void copyUrl() diff --git a/android/app/src/main/java/app/organicmaps/location/LocationSharingNotification.java b/android/app/src/main/java/app/organicmaps/location/LocationSharingNotification.java index 75ce13290..e175f9d48 100644 --- a/android/app/src/main/java/app/organicmaps/location/LocationSharingNotification.java +++ b/android/app/src/main/java/app/organicmaps/location/LocationSharingNotification.java @@ -66,21 +66,19 @@ public class LocationSharingNotification @NonNull public Notification buildNotification(@NonNull PendingIntent stopIntent) { - return buildNotification(stopIntent, null, null); + return buildNotification(stopIntent, null); } /** - * Build notification with current location and routing info. + * Build notification with copy URL action. * @param stopIntent PendingIntent to stop sharing - * @param location Current location (optional) - * @param routingInfo Navigation info (optional) + * @param copyUrlIntent PendingIntent to copy URL (optional) * @return Notification object */ @NonNull public Notification buildNotification( @NonNull PendingIntent stopIntent, - @Nullable Location location, - @Nullable RoutingInfo routingInfo) + @Nullable PendingIntent copyUrlIntent) { Intent notificationIntent = new Intent(mContext, MwmActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity( @@ -90,7 +88,7 @@ public class LocationSharingNotification PendingIntent.FLAG_IMMUTABLE); NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_location_sharing) + .setSmallIcon(R.drawable.ic_share) .setContentIntent(pendingIntent) .setOngoing(true) .setPriority(NotificationCompat.PRIORITY_LOW) @@ -101,17 +99,15 @@ public class LocationSharingNotification // Title builder.setContentTitle(mContext.getString(R.string.location_sharing_active)); - // Content text - String contentText = buildContentText(location, routingInfo); - builder.setContentText(contentText); + // No subtitle - keep it simple - // Big text style for more details - if (routingInfo != null) + // Copy URL action button (if provided) + if (copyUrlIntent != null) { - NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle() - .bigText(contentText) - .setSummaryText(mContext.getString(R.string.location_sharing_tap_to_view)); - builder.setStyle(bigTextStyle); + builder.addAction( + R.drawable.ic_share, + mContext.getString(R.string.location_sharing_copy_url), + copyUrlIntent); } // Stop action button @@ -129,71 +125,6 @@ public class LocationSharingNotification return builder.build(); } - @NonNull - private String buildContentText(@Nullable Location location, @Nullable RoutingInfo routingInfo) - { - StringBuilder text = new StringBuilder(); - - // If navigating, show ETA and distance - if (routingInfo != null && routingInfo.distToTarget != null) - { - if (routingInfo.totalTimeInSeconds > 0) - { - String eta = formatTime(routingInfo.totalTimeInSeconds); - text.append(mContext.getString(R.string.location_sharing_eta, eta)); - } - - if (routingInfo.distToTarget != null && routingInfo.distToTarget.isValid()) - { - if (text.length() > 0) - text.append(" • "); - text.append(routingInfo.distToTarget.toString(mContext)); - text.append(" ").append(mContext.getString(R.string.location_sharing_remaining)); - } - } - else - { - // Standalone mode - show accuracy if available - if (location != null) - { - text.append(mContext.getString(R.string.location_sharing_accuracy, - formatAccuracy(location.getAccuracy()))); - } - else - { - text.append(mContext.getString(R.string.location_sharing_waiting_for_location)); - } - } - - return text.toString(); - } - - @NonNull - private String formatTime(int seconds) - { - if (seconds < 60) - return String.format(Locale.US, "%ds", seconds); - - int minutes = seconds / 60; - if (minutes < 60) - return String.format(Locale.US, "%d min", minutes); - - int hours = minutes / 60; - int remainingMinutes = minutes % 60; - return String.format(Locale.US, "%dh %dm", hours, remainingMinutes); - } - - @NonNull - private String formatAccuracy(float accuracyMeters) - { - if (accuracyMeters < 10) - return mContext.getString(R.string.location_sharing_accuracy_high); - else if (accuracyMeters < 50) - return mContext.getString(R.string.location_sharing_accuracy_medium); - else - return mContext.getString(R.string.location_sharing_accuracy_low); - } - /** * Update existing notification. * @param notificationId Notification ID diff --git a/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java b/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java index b1ba73e29..cfc0a0e2d 100644 --- a/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java +++ b/android/app/src/main/java/app/organicmaps/location/LocationSharingService.java @@ -44,8 +44,9 @@ public class LocationSharingService extends Service implements LocationListener public static final String EXTRA_SERVER_URL = "server_url"; public static final String EXTRA_UPDATE_INTERVAL = "update_interval"; - // Action for notification stop button + // Actions for notification buttons private static final String ACTION_STOP = "app.organicmaps.ACTION_STOP_LOCATION_SHARING"; + private static final String ACTION_COPY_URL = "app.organicmaps.ACTION_COPY_LOCATION_URL"; @Nullable private String mSessionId; @@ -95,6 +96,21 @@ public class LocationSharingService extends Service implements LocationListener return START_NOT_STICKY; } + // Handle copy URL action from notification + if (ACTION_COPY_URL.equals(intent.getAction())) + { + Logger.i(TAG, "Copy URL action received from notification"); + String shareUrl = LocationSharingManager.getInstance().getShareUrl(); + if (shareUrl != null) + { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("Location Share URL", shareUrl); + clipboard.setPrimaryClip(clip); + android.widget.Toast.makeText(this, R.string.location_sharing_url_copied, android.widget.Toast.LENGTH_SHORT).show(); + } + return START_STICKY; + } + // Extract session info mSessionId = intent.getStringExtra(EXTRA_SESSION_ID); mEncryptionKey = intent.getStringExtra(EXTRA_ENCRYPTION_KEY); @@ -129,7 +145,7 @@ public class LocationSharingService extends Service implements LocationListener // Start foreground with notification Notification notification = mNotificationHelper != null - ? mNotificationHelper.buildNotification(getStopIntent()) + ? mNotificationHelper.buildNotification(getStopIntent(), getCopyUrlIntent()) : buildFallbackNotification(); startForeground(NOTIFICATION_ID, notification); @@ -176,15 +192,7 @@ public class LocationSharingService extends Service implements LocationListener { mLastLocation = location; - // Update notification with location info - if (mNotificationHelper != null) - { - Notification notification = mNotificationHelper.buildNotification( - getStopIntent(), - location, - getNavigationInfo()); - mNotificationHelper.updateNotification(NOTIFICATION_ID, notification); - } + // No need to update notification - it's simple and static now // Schedule update if needed scheduleUpdate(); @@ -332,6 +340,15 @@ public class LocationSharingService extends Service implements LocationListener PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } + @NonNull + private PendingIntent getCopyUrlIntent() + { + Intent copyIntent = new Intent(this, LocationSharingService.class); + copyIntent.setAction(ACTION_COPY_URL); + return PendingIntent.getService(this, 1, copyIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + @NonNull private Notification buildFallbackNotification() { @@ -341,8 +358,7 @@ public class LocationSharingService extends Service implements LocationListener return new NotificationCompat.Builder(this, LocationSharingNotification.CHANNEL_ID) .setContentTitle(getString(R.string.location_sharing_active)) - .setContentText(getString(R.string.location_sharing_notification_text)) - .setSmallIcon(R.drawable.ic_location_sharing) + .setSmallIcon(R.drawable.ic_share) .setContentIntent(pendingIntent) .setOngoing(true) .build(); 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 28e760076..6e0eb437b 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsController.java @@ -322,7 +322,9 @@ public class MapButtonsController extends Fragment mBadgeDrawable.setVisible(count > 0); BadgeUtils.attachBadgeDrawable(mBadgeDrawable, menuButton); - updateMenuBadge(TrackRecorder.nativeIsTrackRecordingEnabled()); + final boolean isTrackRecording = TrackRecorder.nativeIsTrackRecordingEnabled(); + final boolean isLocationSharing = app.organicmaps.location.LocationSharingManager.getInstance().isSharing(); + updateMenuBadge(isTrackRecording || isLocationSharing); } 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 7483192c6..6d295f7e7 100644 --- a/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsViewModel.java +++ b/android/app/src/main/java/app/organicmaps/maplayer/MapButtonsViewModel.java @@ -16,6 +16,7 @@ public class MapButtonsViewModel extends ViewModel private final MutableLiveData mSearchOption = new MutableLiveData<>(); private final MutableLiveData mTrackRecorderState = new MutableLiveData<>(TrackRecorder.nativeIsTrackRecordingEnabled()); + private final MutableLiveData mLocationSharingState = new MutableLiveData<>(false); public MutableLiveData getButtonsHidden() { @@ -86,4 +87,14 @@ public class MapButtonsViewModel extends ViewModel { return mTrackRecorderState; } + + public void setLocationSharingState(boolean state) + { + mLocationSharingState.setValue(state); + } + + public MutableLiveData getLocationSharingState() + { + return mLocationSharingState; + } } diff --git a/android/app/src/main/java/app/organicmaps/routing/NavigationController.java b/android/app/src/main/java/app/organicmaps/routing/NavigationController.java index 7ca7e557f..cafc0ec79 100644 --- a/android/app/src/main/java/app/organicmaps/routing/NavigationController.java +++ b/android/app/src/main/java/app/organicmaps/routing/NavigationController.java @@ -205,6 +205,11 @@ public class NavigationController implements TrafficManager.TrafficCallback, Nav mNavMenu.refreshTts(); } + public void refreshShareLocationColor() + { + mNavMenu.updateShareLocationColor(); + } + @Override public void onEnabled() { 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 8a3823789..66e076ea1 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,6 +68,16 @@ public class MenuAdapter extends RecyclerView.Adapter badge.setBackgroundResource(R.drawable.track_recorder_badge); badge.setVisibility(View.VISIBLE); } + + if (item.iconRes == R.drawable.ic_share && app.organicmaps.location.LocationSharingManager.getInstance().isSharing()) + { + // Set icon tint to orange + iv.setImageTintList(android.content.res.ColorStateList.valueOf( + androidx.core.content.ContextCompat.getColor(viewHolder.itemView.getContext(), R.color.active_location_sharing))); + // Show badge + badge.setBackgroundResource(R.drawable.location_sharing_badge); + badge.setVisibility(View.VISIBLE); + } } @Override diff --git a/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java b/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java index f9b2ab6a3..0e34533ba 100644 --- a/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java +++ b/android/app/src/main/java/app/organicmaps/widget/menu/NavMenu.java @@ -27,6 +27,7 @@ public class NavMenu private final View mHeaderFrame; private final ShapeableImageView mTts; + private final ShapeableImageView mShareLocation; private final MaterialTextView mEtaValue; private final MaterialTextView mEtaAmPm; private final MaterialTextView mTimeHourValue; @@ -98,14 +99,16 @@ public class NavMenu mRouteProgress = bottomFrame.findViewById(R.id.navigation_progress); // Bottom frame buttons - ShapeableImageView shareLocation = bottomFrame.findViewById(R.id.share_location); - shareLocation.setOnClickListener(v -> onShareLocationClicked()); + mShareLocation = bottomFrame.findViewById(R.id.share_location); + mShareLocation.setOnClickListener(v -> onShareLocationClicked()); ShapeableImageView mSettings = bottomFrame.findViewById(R.id.settings); mSettings.setOnClickListener(v -> onSettingsClicked()); mTts = bottomFrame.findViewById(R.id.tts_volume); mTts.setOnClickListener(v -> onTtsClicked()); MaterialButton stop = bottomFrame.findViewById(R.id.stop); stop.setOnClickListener(v -> onStopClicked()); + + updateShareLocationColor(); } private void onStopClicked() @@ -116,6 +119,17 @@ public class NavMenu private void onShareLocationClicked() { LocationSharingDialog.show(mActivity.getSupportFragmentManager()); + // Update color after dialog is shown (in case state changes) + mShareLocation.postDelayed(this::updateShareLocationColor, 500); + } + + public void updateShareLocationColor() + { + final boolean isLocationSharing = app.organicmaps.location.LocationSharingManager.getInstance().isSharing(); + final int color = isLocationSharing + ? androidx.core.content.ContextCompat.getColor(mActivity, R.color.active_location_sharing) + : app.organicmaps.util.ThemeUtils.getColor(mActivity, R.attr.iconTint); + mShareLocation.setImageTintList(android.content.res.ColorStateList.valueOf(color)); } private void onSettingsClicked() diff --git a/android/app/src/main/res/drawable/location_sharing_badge.xml b/android/app/src/main/res/drawable/location_sharing_badge.xml new file mode 100644 index 000000000..d58f497fb --- /dev/null +++ b/android/app/src/main/res/drawable/location_sharing_badge.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/android/app/src/main/res/layout/layout_nav_bottom.xml b/android/app/src/main/res/layout/layout_nav_bottom.xml index b093c5111..e65d74853 100644 --- a/android/app/src/main/res/layout/layout_nav_bottom.xml +++ b/android/app/src/main/res/layout/layout_nav_bottom.xml @@ -51,7 +51,7 @@ android:background="?selectableItemBackgroundBorderless" android:scaleType="center" android:contentDescription="@string/location_sharing_title" - app:srcCompat="@drawable/ic_location_sharing" + app:srcCompat="@drawable/ic_share" app:tint="?iconTint" /> diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 717751f5a..be801aa6d 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -124,6 +124,7 @@ @color/base_accent #0057ff + #FF9500 #929292 #FFC22219 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index d74c0bff3..8b520eb70 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -210,6 +210,9 @@ Length Share My Location + Stop Sharing My Location + Share Current Coordinates + Start Live Location Sharing General settings diff --git a/android/app/src/main/res/values/strings_location_sharing.xml b/android/app/src/main/res/values/strings_location_sharing.xml index 08d3f78ed..69e9e11ce 100644 --- a/android/app/src/main/res/values/strings_location_sharing.xml +++ b/android/app/src/main/res/values/strings_location_sharing.xml @@ -4,7 +4,7 @@ Live Location Sharing Start Sharing Stop Sharing - Sharing location + Sharing live location Your location is being shared Location sharing is not active diff --git a/private.h b/private.h index 3bfb82735..e9e227339 100644 --- a/private.h +++ b/private.h @@ -11,4 +11,4 @@ #define TRAFFIC_DATA_BASE_URL "" #define USER_BINDING_PKCS12 "" #define USER_BINDING_PKCS12_PASSWORD "" -#define LOCATION_SHARING_SERVER_URL "https://ec1e1096-e991-4cb1-ac21-30fbad2bd406.mock.pstmn.io" +#define LOCATION_SHARING_SERVER_URL "https://live.comaps.app"