From b0534c44f6dd4ff8ef694acfa16ef7908ca60950 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 6 Mar 2019 12:16:52 +0100 Subject: [PATCH] initial location service implementation, adds a map activity --- AndroidManifest.xml | 24 ++- res/layout/activity_map.xml | 16 ++ res/menu/conversation.xml | 9 +- res/values/attrs.xml | 1 + res/values/strings.xml | 3 + res/values/themes.xml | 2 + .../messenger/geolocation/DcLocation.java | 43 ++++ .../geolocation/DcLocationManager.java | 85 ++++++++ .../LocationBackgroundService.java | 193 ++++++++++++++++++ src/com/b44t/messenger/map/MapActivity.java | 150 ++++++++++++++ .../securesms/ApplicationContext.java | 9 +- .../securesms/ConversationActivity.java | 7 + 12 files changed, 527 insertions(+), 15 deletions(-) create mode 100644 res/layout/activity_map.xml create mode 100644 src/com/b44t/messenger/geolocation/DcLocation.java create mode 100644 src/com/b44t/messenger/geolocation/DcLocationManager.java create mode 100644 src/com/b44t/messenger/geolocation/LocationBackgroundService.java create mode 100644 src/com/b44t/messenger/map/MapActivity.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d791f5155..772a968bd 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,7 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools" + package="org.thoughtcrime.securesms"> @@ -15,21 +15,21 @@ - + - - + + - + - - - + + + @@ -262,7 +262,11 @@ android:theme="@style/TextSecure.LightTheme" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> - + + + + + diff --git a/res/layout/activity_map.xml b/res/layout/activity_map.xml new file mode 100644 index 000000000..06e6cb116 --- /dev/null +++ b/res/layout/activity_map.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/res/menu/conversation.xml b/res/menu/conversation.xml index c9e4de636..128621c72 100644 --- a/res/menu/conversation.xml +++ b/res/menu/conversation.xml @@ -1,5 +1,7 @@ - + @@ -10,4 +12,9 @@ + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 3557f383a..276753894 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -112,6 +112,7 @@ + diff --git a/res/values/strings.xml b/res/values/strings.xml index be91a01ae..83f27a780 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -111,6 +111,7 @@ Send Toggle emoji keyboard Edit group + Show map Archive chat Unarchive chat Add attachment @@ -606,6 +607,7 @@ Open Log Folder Open Current Logfile + Delta Chat needs the location permission in order to show and share your location. @@ -619,4 +621,5 @@ Scan this to join the group \"%1$s\". + diff --git a/res/values/themes.xml b/res/values/themes.xml index ba1c9324a..7bf77fe6a 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -186,6 +186,7 @@ @drawable/ic_add_white_24dp @drawable/ic_group_white_24dp + @android:drawable/ic_dialog_map @drawable/ic_search_white_24dp @drawable/ic_launch_white_24dp @drawable/ic_unlocked_white_24dp @@ -331,6 +332,7 @@ @drawable/ic_add_white_24dp @drawable/ic_group_white_24dp + @android:drawable/ic_dialog_map @drawable/ic_search_white_24dp @drawable/ic_launch_white_24dp @drawable/ic_unlocked_white_24dp diff --git a/src/com/b44t/messenger/geolocation/DcLocation.java b/src/com/b44t/messenger/geolocation/DcLocation.java new file mode 100644 index 000000000..b31fb498f --- /dev/null +++ b/src/com/b44t/messenger/geolocation/DcLocation.java @@ -0,0 +1,43 @@ +package com.b44t.messenger.geolocation; + +import android.location.Location; + +import java.util.Observable; + + +/** + * Created by cyberta on 06.03.19. + */ + +public class DcLocation extends Observable { + private Location lastLocation; + private static DcLocation instance; + + private DcLocation() { + lastLocation = new Location("?"); + } + + public static DcLocation getInstance() { + if (instance == null) { + instance = new DcLocation(); + } + return instance; + } + + public Location getLastLocation() { + return instance.lastLocation; + } + + void updateLocation(Location location) { + lastLocation = location; + + instance.setChanged(); + instance.notifyObservers(); + } + + void reset() { + updateLocation(new Location("?")); + } + + +} diff --git a/src/com/b44t/messenger/geolocation/DcLocationManager.java b/src/com/b44t/messenger/geolocation/DcLocationManager.java new file mode 100644 index 000000000..37a82e994 --- /dev/null +++ b/src/com/b44t/messenger/geolocation/DcLocationManager.java @@ -0,0 +1,85 @@ +package com.b44t.messenger.geolocation; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.location.Location; +import android.os.IBinder; +import android.util.Log; + +import java.util.Observable; +import java.util.Observer; + +import static android.content.Context.BIND_AUTO_CREATE; + +/** + * Created by cyberta on 06.03.19. + */ + +public class DcLocationManager implements Observer { + + private static final String TAG = DcLocationManager.class.getSimpleName(); + private LocationBackgroundService.LocationBackgroundServiceBinder serviceBinder; + private Context context; + private DcLocation dcLocation = DcLocation.getInstance(); + private ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + serviceBinder = (LocationBackgroundService.LocationBackgroundServiceBinder) service; + startLocationEngine(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + serviceBinder = null; + } + }; + + public DcLocationManager(Context context) { + this.context = context.getApplicationContext(); + DcLocation.getInstance().addObserver(this); + } + + + private void initializeLocationEngine() { + Intent intent = new Intent(context.getApplicationContext(), LocationBackgroundService.class); + context.bindService(intent, serviceConnection, BIND_AUTO_CREATE); + } + + public void startLocationEngine() { + if (serviceBinder == null) { + initializeLocationEngine(); + } + } + + public void stopLocationEngine() { + if (serviceBinder == null) { + return; + } + serviceBinder.stop(); + } + + public void shareLastLocation() { + if (serviceBinder == null) { + initializeLocationEngine(); + return; + } + + Location location = dcLocation.getLastLocation(); + Log.d(TAG, "share lastLocation: " + location.getLatitude() + ", " + location.getLongitude()); + //TODO: implement me! write share location message + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof DcLocation) { + dcLocation = (DcLocation) o; + writeDcLocationUpdateMessage(); + } + } + + private void writeDcLocationUpdateMessage() { + //TODO: implement me! + } +} diff --git a/src/com/b44t/messenger/geolocation/LocationBackgroundService.java b/src/com/b44t/messenger/geolocation/LocationBackgroundService.java new file mode 100644 index 000000000..f561ec17a --- /dev/null +++ b/src/com/b44t/messenger/geolocation/LocationBackgroundService.java @@ -0,0 +1,193 @@ +package com.b44t.messenger.geolocation; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +/** + * Created by cyberta on 06.03.19. + */ + +public class LocationBackgroundService extends Service { + + private static final int TIMEOUT = 1000 * 30; + private static final String TAG = LocationBackgroundService.class.getSimpleName(); + private LocationManager locationManager = null; + private static final int LOCATION_INTERVAL = 1000; + private static final float LOCATION_DISTANCE = 20f; + ServiceLocationListener locationListener; + + private final IBinder mBinder = new LocationBackgroundServiceBinder(); + + @Override + public boolean bindService(Intent service, ServiceConnection conn, int flags) { + return super.bindService(service, conn, flags); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onCreate() { + locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE); + if (locationManager == null) { + Log.e(TAG, "Unable to initialize location service"); + return; + } + + locationListener = new ServiceLocationListener(); + requestLocationUpdate(LocationManager.NETWORK_PROVIDER); + requestLocationUpdate(LocationManager.GPS_PROVIDER); + initialLocationUpdate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (locationManager == null) { + return; + } + + try { + locationManager.removeUpdates(locationListener); + } catch (Exception ex) { + Log.i(TAG, "fail to remove location listners, ignore", ex); + } + + + } + + + private void requestLocationUpdate(String provider) { + try { + locationManager.requestLocationUpdates( + provider, LOCATION_INTERVAL, LOCATION_DISTANCE, + locationListener); + } catch (SecurityException | IllegalArgumentException ex) { + Log.e(TAG, String.format("Unable to request %s provider based location updates.", provider), ex); + } + } + + private void initialLocationUpdate() { + try { + Location networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + locationListener.onLocationChanged(networkLocation); + locationListener.onLocationChanged(gpsLocation); + + } catch (NullPointerException | SecurityException e) { + e.printStackTrace(); + } + } + + class LocationBackgroundServiceBinder extends Binder { + LocationBackgroundServiceBinder getService() { + return LocationBackgroundServiceBinder.this; + } + + void stop() { + DcLocation.getInstance().reset(); + stopSelf(); + } + } + + private class ServiceLocationListener implements LocationListener { + + @Override + public void onLocationChanged(Location location) { + Log.e(TAG, "onLocationChanged: " + location); + if (isBetterLocation(location, DcLocation.getInstance().getLastLocation())) { + DcLocation.getInstance().updateLocation(location); + } + } + + @Override + public void onProviderDisabled(String provider) { + Log.e(TAG, "onProviderDisabled: " + provider); + } + + @Override + public void onProviderEnabled(String provider) { + Log.e(TAG, "onProviderEnabled: " + provider); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + Log.e(TAG, "onStatusChanged: " + provider); + } + + + /** https://developer.android.com/guide/topics/location/strategies + * Determines whether one Location reading is better than the current Location fix + * @param location The new Location that you want to evaluate + * @param currentBestLocation The current Location fix, to which you want to compare the new one + */ + private boolean isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { + // A new location is always better than no location + return true; + } + + // Check whether the new location fix is newer or older + long timeDelta = location.getTime() - currentBestLocation.getTime(); + boolean isSignificantlyNewer = timeDelta > TIMEOUT; + boolean isSignificantlyOlder = timeDelta < -TIMEOUT; + boolean isNewer = timeDelta > 0; + + // If it's been more than two minutes since the current location, use the new location + // because the user has likely moved + if (isSignificantlyNewer) { + return true; + // If the new location is more than two minutes older, it must be worse + } else if (isSignificantlyOlder) { + return false; + } + + // Check whether the new location fix is more or less accurate + int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); + boolean isLessAccurate = accuracyDelta > 0; + boolean isMoreAccurate = accuracyDelta < 0; + boolean isSignificantlyLessAccurate = accuracyDelta > 200; + + // Check if the old and new location are from the same provider + boolean isFromSameProvider = isSameProvider(location.getProvider(), + currentBestLocation.getProvider()); + + // Determine location quality using a combination of timeliness and accuracy + if (isMoreAccurate) { + return true; + } else if (isNewer && !isLessAccurate) { + return true; + } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { + return true; + } + return false; + } + + /** Checks whether two providers are the same */ + private boolean isSameProvider(String provider1, String provider2) { + if (provider1 == null) { + return provider2 == null; + } + return provider1.equals(provider2); + } + } + +} diff --git a/src/com/b44t/messenger/map/MapActivity.java b/src/com/b44t/messenger/map/MapActivity.java new file mode 100644 index 000000000..cf209f973 --- /dev/null +++ b/src/com/b44t/messenger/map/MapActivity.java @@ -0,0 +1,150 @@ +package com.b44t.messenger.map; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.widget.Toast; + +import com.b44t.messenger.geolocation.DcLocation; +import com.b44t.messenger.geolocation.DcLocationManager; +import com.mapbox.android.core.permissions.PermissionsListener; +import com.mapbox.android.core.permissions.PermissionsManager; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Style; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.BaseActivity; +import org.thoughtcrime.securesms.R; + +import java.util.List; +import java.util.Observable; +import java.util.Observer; + +public class MapActivity extends BaseActivity implements PermissionsListener, Observer { + + private static final String TAG = MapActivity.class.getSimpleName(); + private MapView mapView; + private PermissionsManager permissionsManager; + private DcLocation dcLocation; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_map); + + mapView = findViewById(R.id.mapView); + mapView.onCreate(savedInstanceState); + mapView.getMapAsync(mapboxMap -> mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> { + + mapboxMap.setCameraPosition(new CameraPosition.Builder() + .target(new LatLng(53.5505D, 10.001D)) + .zoom(12) + .build()); + mapboxMap.getUiSettings().setLogoEnabled(false); + mapboxMap.getUiSettings().setAttributionEnabled(false); + + permissionsManager = new PermissionsManager(this); + + if (PermissionsManager.areLocationPermissionsGranted(this)) { + showDeviceLocation(); + } else { + permissionsManager = new PermissionsManager(this); + permissionsManager.requestLocationPermissions(this); + } + + })); + + dcLocation = DcLocation.getInstance(); + + } + + + @Override + protected void onStart() { + super.onStart(); + mapView.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + mapView.onResume(); + DcLocation.getInstance().addObserver(this); + } + + @Override + protected void onPause() { + super.onPause(); + mapView.onPause(); + DcLocation.getInstance().deleteObserver(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mapView.onDestroy(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + mapView.onLowMemory(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mapView.onSaveInstanceState(outState); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + + //FIXME: only for testing --v + DcLocationManager locationManager = ApplicationContext.getInstance(this).dcLocationManager; + locationManager.stopLocationEngine(); + + } + + //Android SDK callback for the result from requesting permissions + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + //map sdk callbacks for the result from requesting permissions + @Override + public void onPermissionResult(boolean granted) { + if (granted) { + showDeviceLocation(); + } else { + Toast.makeText(this, R.string.user_location_permission_explanation, Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onExplanationNeeded(List permissionsToExplain) { + // no explanation text before the permission pop up is shown + } + + private void showDeviceLocation() { + DcLocationManager locationManager = ApplicationContext.getInstance(this).dcLocationManager; + locationManager.startLocationEngine(); + + } + + + @Override + public void update(Observable o, Object arg) { + if (o instanceof DcLocation) { + this.dcLocation = (DcLocation) o; + Log.d(TAG, "show marker on map: " + dcLocation.getLastLocation().getLatitude() + ", " + dcLocation.getLastLocation().getLongitude()); + + } + } +} diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index eb277987d..e3adf67ca 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -26,6 +26,7 @@ import android.support.annotation.NonNull; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEventCenter; +import com.b44t.messenger.geolocation.DcLocationManager; import com.mapbox.mapboxsdk.Mapbox; import org.thoughtcrime.securesms.connect.ApplicationDcContext; @@ -50,11 +51,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private static final String TAG = ApplicationContext.class.getName(); - private JobManager jobManager; - - private volatile boolean isAppVisible; - public ApplicationDcContext dcContext; + public DcLocationManager dcLocationManager; + private JobManager jobManager; + private volatile boolean isAppVisible; public static ApplicationContext getInstance(Context context) { return (ApplicationContext)context.getApplicationContext(); @@ -73,6 +73,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO initializeIncomingMessageNotifier(); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); Mapbox.getInstance(getApplicationContext(), BuildConfig.MAP_ACCESS_TOKEN); + dcLocationManager = new DcLocationManager(this); } @Override diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index f8da81b7d..7a0375a92 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -63,6 +63,7 @@ import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEventCenter; import com.b44t.messenger.DcMsg; +import com.b44t.messenger.map.MapActivity; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.audio.AudioRecorder; @@ -446,6 +447,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity case R.id.menu_mute_notifications: handleMuteNotifications(); return true; case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true; case R.id.menu_conversation_settings: handleConversationSettings(); return true; + case R.id.menu_show_map: handleShowMap(); return true; case android.R.id.home: handleReturnToConversationList(); return true; } @@ -471,6 +473,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity //////// Event Handlers + private void handleShowMap() { + Intent intent = new Intent(this, MapActivity.class); + startActivity(intent); + } + private void handleReturnToConversationList() { Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class)); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);