() {
+ @Override
+ public AddressData createFromParcel(Parcel in) {
+ return new AddressData(in.readDouble(),
+ in.readDouble(),
+ Address.CREATOR.createFromParcel(in));
+ }
+
+ @Override
+ public AddressData[] newArray(int size) {
+ return new AddressData[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeDouble(latitude);
+ dest.writeDouble(longitude);
+ dest.writeParcelable(address, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/maps/PlacePickerActivity.java b/src/org/thoughtcrime/securesms/maps/PlacePickerActivity.java
new file mode 100644
index 0000000000..c5d14e26da
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/maps/PlacePickerActivity.java
@@ -0,0 +1,243 @@
+package org.thoughtcrime.securesms.maps;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.location.Address;
+import android.location.Geocoder;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.animation.OvershootInterpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.gms.location.FusedLocationProviderClient;
+import com.google.android.gms.location.LocationServices;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.logging.Log;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Allows selection of an address from a google map.
+ *
+ * Based on https://github.com/suchoX/PlacePicker
+ */
+public final class PlacePickerActivity extends AppCompatActivity {
+
+ private static final String TAG = Log.tag(PlacePickerActivity.class);
+
+ // If it cannot load location for any reason, it defaults to the prime meridian.
+ private static final LatLng PRIME_MERIDIAN = new LatLng(51.4779, -0.0015);
+ private static final String ADDRESS_INTENT = "ADDRESS";
+ private static final float ZOOM = 17.0f;
+
+ private static final int ANIMATION_DURATION = 250;
+ private static final OvershootInterpolator OVERSHOOT_INTERPOLATOR = new OvershootInterpolator();
+
+ private SingleAddressBottomSheet bottomSheet;
+ private Address currentAddress;
+ private LatLng initialLocation;
+ private LatLng currentLocation = new LatLng(0, 0);
+ private AddressLookup addressLookup;
+ private GoogleMap googleMap;
+
+ public static void startActivityForResultAtCurrentLocation(@NonNull Activity activity, int requestCode) {
+ activity.startActivityForResult(new Intent(activity, PlacePickerActivity.class), requestCode);
+ }
+
+ public static AddressData addressFromData(@NonNull Intent data) {
+ return data.getParcelableExtra(ADDRESS_INTENT);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_place_picker);
+
+ bottomSheet = findViewById(R.id.bottom_sheet);
+ View markerImage = findViewById(R.id.marker_image_view);
+ View fab = findViewById(R.id.place_chosen_button);
+
+ fab.setOnClickListener(v -> finishWithAddress());
+
+ FusedLocationProviderClient fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
+ checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
+ checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
+ ) {
+ fusedLocationClient.getLastLocation()
+ .addOnFailureListener(e -> {
+ Log.e(TAG, "Failed to get location", e);
+ setInitialLocation(PRIME_MERIDIAN);
+ })
+ .addOnSuccessListener(location -> {
+ if (location == null) {
+ Log.w(TAG, "Failed to get location");
+ setInitialLocation(PRIME_MERIDIAN);
+ } else {
+ setInitialLocation(new LatLng(location.getLatitude(), location.getLongitude()));
+ }
+ });
+ } else {
+ Log.w(TAG, "No location permissions");
+ setInitialLocation(PRIME_MERIDIAN);
+ }
+
+ SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
+ if (mapFragment == null) throw new AssertionError("No map fragment");
+
+ mapFragment.getMapAsync(googleMap -> {
+
+ setMap(googleMap);
+
+ enableMyLocationButtonIfHaveThePermission(googleMap);
+
+ googleMap.setOnCameraMoveStartedListener(i -> {
+ markerImage.animate()
+ .translationY(-75f)
+ .setInterpolator(OVERSHOOT_INTERPOLATOR)
+ .setDuration(ANIMATION_DURATION)
+ .start();
+
+ bottomSheet.hide();
+ });
+
+ googleMap.setOnCameraIdleListener(() -> {
+ markerImage.animate()
+ .translationY(0f)
+ .setInterpolator(OVERSHOOT_INTERPOLATOR)
+ .setDuration(ANIMATION_DURATION)
+ .start();
+
+ setCurrentLocation(googleMap.getCameraPosition().target);
+ });
+ });
+ }
+
+ private void setInitialLocation(@NonNull LatLng latLng) {
+ initialLocation = latLng;
+
+ moveMapToInitialIfPossible();
+ }
+
+ private void setMap(GoogleMap googleMap) {
+ this.googleMap = googleMap;
+
+ moveMapToInitialIfPossible();
+ }
+
+ private void moveMapToInitialIfPossible() {
+ if (initialLocation != null && googleMap != null) {
+ Log.d(TAG, "Moving map to initial location");
+ googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(initialLocation, ZOOM));
+ setCurrentLocation(initialLocation);
+ }
+ }
+
+ private void setCurrentLocation(LatLng location) {
+ currentLocation = location;
+ bottomSheet.showLoading();
+ lookupAddress(location);
+ }
+
+ private void finishWithAddress() {
+ Intent returnIntent = new Intent();
+ AddressData addressData = new AddressData(currentLocation.latitude, currentLocation.longitude, currentAddress);
+ returnIntent.putExtra(ADDRESS_INTENT, addressData);
+ setResult(RESULT_OK, returnIntent);
+ finish();
+ }
+
+ private void enableMyLocationButtonIfHaveThePermission(GoogleMap googleMap) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
+ checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
+ checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED)
+ {
+ googleMap.setMyLocationEnabled(true);
+ }
+ }
+
+ private void lookupAddress(@Nullable LatLng target) {
+ if (addressLookup != null) {
+ addressLookup.cancel(true);
+ }
+ addressLookup = new AddressLookup();
+ addressLookup.execute(target);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (addressLookup != null) {
+ addressLookup.cancel(true);
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private class AddressLookup extends AsyncTask {
+
+ private final String TAG = Log.tag(AddressLookup.class);
+ private final Geocoder geocoder;
+
+ AddressLookup() {
+ geocoder = new Geocoder(getApplicationContext(), Locale.getDefault());
+ }
+
+ @Override
+ protected Address doInBackground(LatLng... latLngs) {
+ if (latLngs.length == 0) return null;
+ LatLng latLng = latLngs[0];
+ if (latLng == null) return null;
+ try {
+ List result = geocoder.getFromLocation(latLng.latitude, latLng.longitude, 1);
+ return !result.isEmpty() ? result.get(0) : null;
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to get address from location", e);
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(@Nullable Address address) {
+ Log.d(TAG, String.format("%s", addressToString(address)));
+ currentAddress = address;
+ if (address != null) {
+ bottomSheet.showResult(address.getLatitude(), address.getLongitude(), addressToShortString(address), addressToString(address));
+ } else {
+ bottomSheet.hide();
+ }
+ }
+ }
+
+ private static @NonNull String addressToString(@Nullable Address address) {
+ return address != null ? address.getAddressLine(0) : "";
+ }
+
+ private static @NonNull String addressToShortString(@Nullable Address address) {
+ if (address == null) return "";
+
+ String addressLine = address.getAddressLine(0);
+ String[] split = addressLine.split(",");
+
+ if (split.length >= 3) {
+ return split[1].trim() + ", " + split[2].trim();
+ } else if (split.length == 2) {
+ return split[1].trim();
+ } else return split[0].trim();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/maps/SingleAddressBottomSheet.java b/src/org/thoughtcrime/securesms/maps/SingleAddressBottomSheet.java
new file mode 100644
index 0000000000..d1de0512e2
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/maps/SingleAddressBottomSheet.java
@@ -0,0 +1,81 @@
+package org.thoughtcrime.securesms.maps;
+
+import android.content.Context;
+import android.location.Location;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import org.thoughtcrime.securesms.R;
+
+import java.util.Locale;
+
+final class SingleAddressBottomSheet extends CoordinatorLayout {
+
+ private TextView placeNameTextView;
+ private TextView placeAddressTextView;
+ private ProgressBar placeProgressBar;
+ private BottomSheetBehavior bottomSheetBehavior;
+
+ public SingleAddressBottomSheet(@NonNull Context context) {
+ super(context);
+ init();
+ }
+
+ public SingleAddressBottomSheet(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public SingleAddressBottomSheet(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ CoordinatorLayout rootView = (CoordinatorLayout) inflate(getContext(), R.layout.activity_map_bottom_sheet_view, this);
+
+ bottomSheetBehavior = BottomSheetBehavior.from(rootView.findViewById(R.id.root_bottom_sheet));
+ bottomSheetBehavior.setHideable(true);
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+
+ bindViews();
+ }
+
+ private void bindViews() {
+ placeNameTextView = findViewById(R.id.text_view_place_name);
+ placeAddressTextView = findViewById(R.id.text_view_place_address);
+ placeProgressBar = findViewById(R.id.progress_bar_place);
+ }
+
+ public void showLoading() {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ placeNameTextView.setText("");
+ placeAddressTextView.setText("");
+ placeProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ public void showResult(double latitude, double longitude, String addressToShortString, String addressToString) {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ placeProgressBar.setVisibility(View.GONE);
+
+ if (TextUtils.isEmpty(addressToString)) {
+ String longString = Location.convert(longitude, Location.FORMAT_DEGREES);
+ String latString = Location.convert(latitude, Location.FORMAT_DEGREES);
+
+ placeNameTextView.setText(String.format(Locale.getDefault(), "%s %s", latString, longString));
+ } else {
+ placeNameTextView.setText(addressToShortString);
+ placeAddressTextView.setText(addressToString);
+ }
+ }
+
+ public void hide() {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index 64478d595f..53bd324999 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -30,23 +30,17 @@ import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import android.text.TextUtils;
-
-import org.thoughtcrime.securesms.TransportOption;
-import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
-import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair;
import android.view.View;
import android.widget.Toast;
-import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
-import com.google.android.gms.common.GooglePlayServicesRepairableException;
-import com.google.android.gms.location.places.ui.PlacePicker;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
@@ -55,6 +49,9 @@ import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.maps.PlacePickerActivity;
+import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
@@ -414,13 +411,7 @@ public class AttachmentManager {
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_location_information_in_order_to_attach_a_location))
- .onAllGranted(() -> {
- try {
- activity.startActivityForResult(new PlacePicker.IntentBuilder().build(activity), requestCode);
- } catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException e) {
- Log.w(TAG, e);
- }
- })
+ .onAllGranted(() -> PlacePickerActivity.startActivityForResultAtCurrentLocation(activity, requestCode))
.execute();
}