Indicate when location sharing is active

Signed-off-by: zyphlar <zyphlar@gmail.com>
This commit is contained in:
zyphlar
2025-10-24 15:33:02 -07:00
parent 6c3710859b
commit 7e75aac135
16 changed files with 168 additions and 112 deletions

View File

@@ -425,13 +425,18 @@ 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;
}
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)
@@ -439,6 +444,42 @@ public class MwmActivity extends BaseMwmFragmentActivity
.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))

View File

@@ -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()

View File

@@ -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

View File

@@ -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();

View File

@@ -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()

View File

@@ -16,6 +16,7 @@ public class MapButtonsViewModel extends ViewModel
private final MutableLiveData<SearchWheel.SearchOption> mSearchOption = new MutableLiveData<>();
private final MutableLiveData<Boolean> mTrackRecorderState =
new MutableLiveData<>(TrackRecorder.nativeIsTrackRecordingEnabled());
private final MutableLiveData<Boolean> mLocationSharingState = new MutableLiveData<>(false);
public MutableLiveData<Boolean> getButtonsHidden()
{
@@ -86,4 +87,14 @@ public class MapButtonsViewModel extends ViewModel
{
return mTrackRecorderState;
}
public void setLocationSharingState(boolean state)
{
mLocationSharingState.setValue(state);
}
public MutableLiveData<Boolean> getLocationSharingState()
{
return mLocationSharingState;
}
}

View File

@@ -205,6 +205,11 @@ public class NavigationController implements TrafficManager.TrafficCallback, Nav
mNavMenu.refreshTts();
}
public void refreshShareLocationColor()
{
mNavMenu.updateShareLocationColor();
}
@Override
public void onEnabled()
{

View File

@@ -68,6 +68,16 @@ public class MenuAdapter extends RecyclerView.Adapter<MenuAdapter.ViewHolder>
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

View File

@@ -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()

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="12dp" />
<solid android:color="@color/active_location_sharing" />
</shape>

View File

@@ -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" />
<com.google.android.material.imageview.ShapeableImageView

View File

@@ -76,7 +76,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_base"
app:srcCompat="@drawable/ic_location_sharing"
app:srcCompat="@drawable/ic_share"
app:tint="?colorSecondary"
android:contentDescription="@string/location_sharing_title"/>
</LinearLayout>

View File

@@ -124,6 +124,7 @@
<color name="elevation_profile">@color/base_accent</color>
<color name="active_track_recording">#0057ff</color>
<color name="active_location_sharing">#FF9500</color>
<color name="material_calendar_surface_dark">#929292</color>
<color name="notification_warning">#FFC22219</color>

View File

@@ -210,6 +210,9 @@
<!-- Length of track in cell that describes route -->
<string name="length">Length</string>
<string name="share_my_location">Share My Location</string>
<string name="stop_sharing_my_location">Stop Sharing My Location</string>
<string name="share_location_coordinates">Share Current Coordinates</string>
<string name="share_location_live">Start Live Location Sharing</string>
<!-- Settings general group in settings screen -->
<string name="prefs_group_general">General settings</string>
<!-- Settings information group in settings screen -->

View File

@@ -4,7 +4,7 @@
<string name="location_sharing_title">Live Location Sharing</string>
<string name="location_sharing_start">Start Sharing</string>
<string name="location_sharing_stop">Stop Sharing</string>
<string name="location_sharing_active">Sharing location</string>
<string name="location_sharing_active">Sharing live location</string>
<string name="location_sharing_status_active">Your location is being shared</string>
<string name="location_sharing_status_inactive">Location sharing is not active</string>

View File

@@ -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"