diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index c6a3ba9ba..daa33162c 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -39,6 +39,7 @@
+
diff --git a/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java b/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java
index 1876caa2c..a85696ad5 100644
--- a/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java
+++ b/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java
@@ -5,9 +5,12 @@
import android.content.Intent;
import android.content.ServiceConnection;
import android.location.Location;
+import android.os.Build;
import android.os.IBinder;
import android.util.Log;
+import androidx.core.content.ContextCompat;
+
import org.thoughtcrime.securesms.connect.DcHelper;
import java.util.LinkedList;
@@ -23,11 +26,13 @@ public class DcLocationManager implements Observer {
private final Context context;
private DcLocation dcLocation = DcLocation.getInstance();
private final LinkedList pendingShareLastLocation = new LinkedList<>();
+ private boolean serviceBound = false;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "background service connected");
serviceBinder = (LocationBackgroundService.LocationBackgroundServiceBinder) service;
+ serviceBound = true;
while (!pendingShareLastLocation.isEmpty()) {
shareLastLocation(pendingShareLastLocation.pop());
}
@@ -37,6 +42,7 @@ public void onServiceConnected(ComponentName name, IBinder service) {
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "background service disconnected");
serviceBinder = null;
+ serviceBound = false;
}
};
@@ -49,19 +55,29 @@ public DcLocationManager(Context context) {
}
public void startLocationEngine() {
- if (serviceBinder == null) {
+ if (serviceBinder == null || !serviceBound) {
Intent intent = new Intent(context.getApplicationContext(), LocationBackgroundService.class);
+ // Start as foreground service
+ ContextCompat.startForegroundService(context, intent);
+ // Then bind to it
context.bindService(intent, serviceConnection, BIND_AUTO_CREATE);
}
}
public void stopLocationEngine() {
- if (serviceBinder == null) {
+ if (serviceBinder == null || !serviceBound) {
return;
}
- context.unbindService(serviceConnection);
- serviceBinder.stop();
+ try {
+ context.unbindService(serviceConnection);
+ if (serviceBinder != null) {
+ serviceBinder.stop();
+ }
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Service not registered", e);
+ }
serviceBinder = null;
+ serviceBound = false;
}
public void stopSharingLocation(int chatId) {
diff --git a/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java b/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java
index 4b17f1d27..7a8bfb50b 100644
--- a/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java
+++ b/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java
@@ -1,18 +1,36 @@
package org.thoughtcrime.securesms.geolocation;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.ServiceInfo;
import android.location.Location;
-import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Binder;
-import android.os.Bundle;
+import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.ServiceCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.location.LocationListenerCompat;
+import androidx.core.location.LocationManagerCompat;
+import androidx.core.location.LocationRequestCompat;
+
+import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.notifications.NotificationCenter;
+import org.thoughtcrime.securesms.util.IntentUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
public class LocationBackgroundService extends Service {
@@ -22,6 +40,7 @@ public class LocationBackgroundService extends Service {
private static final int LOCATION_INTERVAL = 1000;
private static final float LOCATION_DISTANCE = 25F;
ServiceLocationListener locationListener;
+ private final AtomicBoolean isForeground = new AtomicBoolean(false);
private final IBinder mBinder = new LocationBackgroundServiceBinder();
@@ -37,12 +56,21 @@ public IBinder onBind(Intent intent) {
@Override
public void onCreate() {
+ super.onCreate();
+
locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
if (locationManager == null) {
Log.e(TAG, "Unable to initialize location service");
+ // Must start foreground to avoid crash, then stop immediately
+ initializeForegroundService();
+ stopForeground(true);
+ stopSelf();
return;
}
+ // Initialize foreground service after successful location manager setup
+ initializeForegroundService();
+
locationListener = new ServiceLocationListener();
Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastLocation != null) {
@@ -51,14 +79,19 @@ public void onCreate() {
DcLocation.getInstance().updateLocation(lastLocation);
}
}
- //requestLocationUpdate(LocationManager.NETWORK_PROVIDER);
+ // Request location updates from both GPS and network providers for better coverage
requestLocationUpdate(LocationManager.GPS_PROVIDER);
+ requestLocationUpdate(LocationManager.NETWORK_PROVIDER);
initialLocationUpdate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
+
+ // Ensure foreground notification is shown (handles edge cases)
+ initializeForegroundService();
+
return START_STICKY;
}
@@ -66,23 +99,93 @@ public int onStartCommand(Intent intent, int flags, int startId) {
public void onDestroy() {
super.onDestroy();
- if (locationManager == null) {
+ // Stop foreground notification
+ stopForeground(true);
+
+ if (locationManager == null || locationListener == null) {
return;
}
try {
- locationManager.removeUpdates(locationListener);
+ LocationManagerCompat.removeUpdates(locationManager, locationListener);
} catch (Exception ex) {
Log.i(TAG, "fail to remove location listeners, ignore", ex);
}
}
+ private void initializeForegroundService() {
+ if (isForeground.compareAndSet(false, true)) {
+ createNotificationChannel();
+ Notification notification = createNotification();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // Android 14+ requires foregroundServiceType in startForeground
+ ServiceCompat.startForeground(this, NotificationCenter.ID_LOCATION, notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
+ } else {
+ startForeground(NotificationCenter.ID_LOCATION, notification);
+ }
+ Log.d(TAG, "Foreground service started with notification");
+ }
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ NotificationCenter.CH_LOCATION,
+ getString(R.string.location),
+ NotificationManager.IMPORTANCE_LOW
+ );
+ channel.setDescription("Location sharing notification");
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+ }
+
+ private Notification createNotification() {
+ Intent intent = new Intent(this, ConversationListActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ IntentUtils.FLAG_IMMUTABLE()
+ );
+
+ return new NotificationCompat.Builder(this, NotificationCenter.CH_LOCATION)
+ .setContentTitle(getString(R.string.location_sharing_notification_title))
+ .setContentText(getString(R.string.location_sharing_notification_text))
+ .setSmallIcon(R.drawable.ic_location_on_white_24dp)
+ .setContentIntent(pendingIntent)
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build();
+ }
+
private void requestLocationUpdate(String provider) {
try {
- locationManager.requestLocationUpdates(
- provider, LOCATION_INTERVAL, LOCATION_DISTANCE,
- locationListener);
- } catch (SecurityException | IllegalArgumentException ex) {
+ // Check if provider is available
+ if (!locationManager.isProviderEnabled(provider)) {
+ Log.w(TAG, String.format("Provider %s is not enabled", provider));
+ return;
+ }
+
+ // Use LocationManagerCompat for better compatibility with modern Android
+ LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(LOCATION_INTERVAL)
+ .setMinUpdateDistanceMeters(LOCATION_DISTANCE)
+ .setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
+ .build();
+
+ Executor executor = ContextCompat.getMainExecutor(this);
+ LocationManagerCompat.requestLocationUpdates(
+ locationManager,
+ provider,
+ locationRequest,
+ executor,
+ locationListener
+ );
+ Log.d(TAG, String.format("Requested location updates from %s provider", provider));
+ } catch (SecurityException | IllegalArgumentException ex) {
Log.e(TAG, String.format("Unable to request %s provider based location updates.", provider), ex);
}
}
@@ -93,9 +196,16 @@ private void initialLocationUpdate() {
if (gpsLocation != null && System.currentTimeMillis() - gpsLocation.getTime() < INITIAL_TIMEOUT) {
locationListener.onLocationChanged(gpsLocation);
}
-
+ // Also try network provider for initial location
+ Location networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+ if (networkLocation != null && System.currentTimeMillis() - networkLocation.getTime() < INITIAL_TIMEOUT) {
+ // Use network location if GPS location is not available or network location is newer
+ if (gpsLocation == null || networkLocation.getTime() > gpsLocation.getTime()) {
+ locationListener.onLocationChanged(networkLocation);
+ }
+ }
} catch (NullPointerException | SecurityException e) {
- e.printStackTrace();
+ Log.e(TAG, "Error getting initial location", e);
}
}
@@ -110,30 +220,22 @@ void stop() {
}
}
- private class ServiceLocationListener implements LocationListener {
+ private class ServiceLocationListener implements LocationListenerCompat {
@Override
public void onLocationChanged(@NonNull Location location) {
Log.d(TAG, "onLocationChanged: " + location);
- if (location == null) {
- return;
- }
DcLocation.getInstance().updateLocation(location);
}
@Override
public void onProviderDisabled(@NonNull String provider) {
- Log.e(TAG, "onProviderDisabled: " + provider);
+ Log.w(TAG, "onProviderDisabled: " + provider);
}
@Override
public void onProviderEnabled(@NonNull String provider) {
- Log.e(TAG, "onProviderEnabled: " + provider);
- }
-
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) {
- Log.e(TAG, "onStatusChanged: " + provider + " status: " + status);
+ Log.d(TAG, "onProviderEnabled: " + provider);
}
}
diff --git a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
index c0dde8289..787d34d85 100644
--- a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -484,7 +484,7 @@ public static void selectLocation(Activity activity, int chatId) {
// for rationale dialog requirements
Permissions.PermissionsBuilder permissionsBuilder = Permissions.with(activity)
.ifNecessary()
- .withRationaleDialog("To share your live location with chat members, allow ArcaneChat to use your location data.\n\nTo make live location work gaplessly, location data is used even when the app is closed or not in use.", R.drawable.ic_location_on_white_24dp)
+ .withRationaleDialog("To share your live location with chat members, allow ArcaneChat to use your location data.", R.drawable.ic_location_on_white_24dp)
.withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_location_denied))
.onAllGranted(() -> {
ShareLocationDialog.show(activity, durationInSeconds -> {
@@ -495,11 +495,7 @@ public static void selectLocation(Activity activity, int chatId) {
}
});
});
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
- permissionsBuilder.request(Manifest.permission.ACCESS_BACKGROUND_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
- } else {
- permissionsBuilder.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
- }
+ permissionsBuilder.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
permissionsBuilder.execute();
}
diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java b/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java
index 09e94c250..170abb27f 100644
--- a/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java
+++ b/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCenter.java
@@ -218,6 +218,7 @@ public PendingIntent getDeclineCallIntent(ChatData chatData, int callId) {
public static final int ID_MSG_SUMMARY = 2;
public static final int ID_GENERIC = 3;
public static final int ID_FETCH = 4;
+ public static final int ID_LOCATION = 5;
public static final int ID_MSG_OFFSET = 0; // msgId is added - as msgId start at 10, there are no conflicts with lower numbers
@@ -243,6 +244,7 @@ public PendingIntent getDeclineCallIntent(ChatData chatData, int callId) {
public static final String CH_MSG_VERSION = "5";
public static final String CH_PERMANENT = "dc_fg_notification_ch";
public static final String CH_GENERIC = "ch_generic";
+ public static final String CH_LOCATION = "ch_location";
public static final String CH_CALLS_PREFIX = "call_chan";
private boolean notificationChannelsSupported() {
diff --git a/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java b/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java
index 70c4544cc..895eb0bbb 100644
--- a/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java
+++ b/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java
@@ -39,4 +39,12 @@ public static int FLAG_MUTABLE() {
return 0;
}
}
+
+ public static int FLAG_IMMUTABLE() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return PendingIntent.FLAG_IMMUTABLE;
+ } else {
+ return 0;
+ }
+ }
}
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 44cb825db..f02b37678 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -358,6 +358,8 @@
Copy JSON
Replace Draft
Share location with all group members
+ Sharing location
+ Location is being shared with chat members
Device Messages
Locally generated messages
Messages in this chat are generated on your device to inform about app updates and problems during usage.