Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,11 +26,13 @@ public class DcLocationManager implements Observer {
private final Context context;
private DcLocation dcLocation = DcLocation.getInstance();
private final LinkedList<Integer> 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());
}
Expand All @@ -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;
}
};

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

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

Expand All @@ -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) {
Expand All @@ -51,38 +79,113 @@ 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;
}

@Override
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);
}
}
Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
2 changes: 2 additions & 0 deletions src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@
<string name="copy_json">Copy JSON</string>
<string name="replace_draft">Replace Draft</string>
<string name="title_share_location">Share location with all group members</string>
<string name="location_sharing_notification_title">Sharing location</string>
<string name="location_sharing_notification_text">Location is being shared with chat members</string>
<string name="device_talk">Device Messages</string>
<string name="device_talk_subtitle">Locally generated messages</string>
<string name="device_talk_explain">Messages in this chat are generated on your device to inform about app updates and problems during usage.</string>
Expand Down
Loading