diff --git a/ring-android/app/build.gradle b/ring-android/app/build.gradle
index 64f2d847b0f6d71f33ce3e2d983ea7891b57065c..23de60015db4fb82c2b96ae476337ebd5b3307a6 100644
--- a/ring-android/app/build.gradle
+++ b/ring-android/app/build.gradle
@@ -89,6 +89,7 @@ dependencies {
     implementation "androidx.media:media:$android_support_core_version"
     implementation "com.google.android.material:material:$material_version"
     implementation 'com.google.android:flexbox:1.1.1'
+    implementation 'org.osmdroid:osmdroid-android:6.1.5'
 
     // ORM
     implementation 'com.j256.ormlite:ormlite-android:5.1'
diff --git a/ring-android/app/src/main/AndroidManifest.xml b/ring-android/app/src/main/AndroidManifest.xml
index 9e0e5561e5e74da54935fe3a46a308f273b687d3..97aacf29da19fbce7b8017a9b72b7714461700f1 100644
--- a/ring-android/app/src/main/AndroidManifest.xml
+++ b/ring-android/app/src/main/AndroidManifest.xml
@@ -22,6 +22,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     package="cx.ring"
     android:installLocation="auto">
 
+    <uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
+
     <supports-screens
         android:anyDensity="true"
         android:largeScreens="true"
@@ -46,7 +48,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
-    <uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 
     <uses-feature
         android:name="android.hardware.wifi"
@@ -72,6 +74,9 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     <uses-feature
         android:name="android.hardware.usb.host"
         android:required="false" />
+    <uses-feature
+        android:name="android.hardware.location.gps"
+        android:required="false" />
 
     <application
         android:allowBackup="false"
@@ -182,7 +187,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
             android:name=".services.SyncService"
             android:description="@string/data_transfer_service_description"
             android:exported="false"
-            android:foregroundServiceType="dataSync"/>
+            android:foregroundServiceType="dataSync" />
+        <service
+            android:name=".services.LocationSharingService"
+            android:foregroundServiceType="location"
+            android:exported="false" />
 
         <activity
             android:name=".client.CallActivity"
diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
index 16fc2e23e1facaceae36680db26d1c4ea3a7369b..68a3d8cae411ec8d001c96fd57859993cfb42b6a 100644
--- a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
@@ -109,7 +109,7 @@ public class ConversationActivity extends AppCompatActivity implements Colorable
                     .replace(R.id.main_frame, mConversationFragment, null)
                     .commit();
         }
-        if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+        if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action) || Intent.ACTION_VIEW.equals(action)) {
             mPendingIntent = intent;
         }
     }
diff --git a/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java b/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java
index 2f83e8f3362aafc4a46dd087dddc8d518f01e430..f6cdd0ee45e18cbd72800c59bd5c9f60d8c90c65 100644
--- a/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java
+++ b/ring-android/app/src/main/java/cx/ring/contacts/AvatarFactory.java
@@ -50,18 +50,25 @@ public class AvatarFactory {
         getGlideAvatar(view.getContext(), contact).into(view);
     }
 
-    public static Single<Drawable> getAvatar(Context context, CallContact contact) {
+    public static Single<Drawable> getAvatar(Context context, CallContact contact, boolean presence) {
         return Single.fromCallable(() ->
                 new AvatarDrawable.Builder()
                         .withContact(contact)
                         .withCircleCrop(true)
+                        .withPresence(presence)
                         .build(context));
     }
+    public static Single<Drawable> getAvatar(Context context, CallContact contact) {
+        return getAvatar(context, contact, true);
+    }
 
-    public static Single<Bitmap> getBitmapAvatar(Context context, CallContact contact, int size) {
-        return getAvatar(context, contact)
+    public static Single<Bitmap> getBitmapAvatar(Context context, CallContact contact, int size, boolean presence) {
+        return getAvatar(context, contact, presence)
                 .map(d -> drawableToBitmap(d, size));
     }
+    public static Single<Bitmap> getBitmapAvatar(Context context, CallContact contact, int size) {
+        return getBitmapAvatar(context, contact, size, true);
+    }
 
     public static Single<Bitmap> getBitmapAvatar(Context context, Account account, int size) {
         return AvatarDrawable.load(context, account)
diff --git a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java
index 421215e779dd7d24051db642e443a7ddab23093a..3df267b2c044fd152b71937e276448213a3253ae 100755
--- a/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java
+++ b/ring-android/app/src/main/java/cx/ring/dependencyinjection/JamiInjectionComponent.java
@@ -22,6 +22,7 @@ package cx.ring.dependencyinjection;
 import javax.inject.Singleton;
 
 import cx.ring.account.AccountEditionFragment;
+import cx.ring.fragments.LocationSharingFragment;
 import cx.ring.account.AccountWizardActivity;
 import cx.ring.account.HomeAccountCreationFragment;
 import cx.ring.account.JamiAccountConnectFragment;
@@ -63,6 +64,7 @@ import cx.ring.services.DataTransferService;
 import cx.ring.services.DeviceRuntimeServiceImpl;
 import cx.ring.services.HardwareService;
 import cx.ring.services.HistoryServiceImpl;
+import cx.ring.services.LocationSharingService;
 import cx.ring.services.NotificationServiceImpl;
 import cx.ring.services.JamiChooserTargetService;
 import cx.ring.services.SharedPreferencesServiceImpl;
@@ -212,11 +214,15 @@ public interface JamiInjectionComponent {
 
     void inject(JamiChooserTargetService service);
 
-    void inject(ShareWithFragment fragment);
+    void inject(LocationSharingFragment service);
+
+    void inject(JamiJobService service);
 
-    void inject(JamiJobService fragment);
+    void inject(ShareWithFragment fragment);
 
     void inject(ContactDetailsActivity fragment);
 
     void inject(IconCardPresenter presenter);
+
+    void inject(LocationSharingService service);
 }
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
index 759d55ab76459be0d63fba183d08103023f3adc3..13f139e8d1f754aa431d06b6669b74836a5ab221 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/ConversationFragment.java
@@ -20,15 +20,21 @@
 package cx.ring.fragments;
 
 import android.Manifest;
+import android.animation.LayoutTransition;
 import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.graphics.Typeface;
+import android.content.res.Resources;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.provider.MediaStore;
 import android.text.Editable;
 import android.text.TextUtils;
@@ -58,6 +64,8 @@ import androidx.appcompat.view.menu.MenuPopupHelper;
 import androidx.appcompat.widget.PopupMenu;
 import androidx.appcompat.widget.Toolbar;
 import androidx.core.view.ViewCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
 import androidx.recyclerview.widget.DefaultItemAnimator;
 
 import java.io.File;
@@ -90,11 +98,13 @@ import cx.ring.model.Phone;
 import cx.ring.model.Error;
 import cx.ring.model.Uri;
 import cx.ring.mvp.BaseSupportFragment;
+import cx.ring.services.LocationSharingService;
 import cx.ring.services.NotificationService;
 import cx.ring.utils.ActionHelper;
 import cx.ring.utils.AndroidFileUtils;
 import cx.ring.utils.ContentUriHandler;
 import cx.ring.utils.DeviceUtils;
+import cx.ring.utils.ConversationPath;
 import cx.ring.utils.MediaButtonsHelper;
 import cx.ring.views.AvatarDrawable;
 import io.reactivex.Completable;
@@ -107,7 +117,7 @@ import static android.app.Activity.RESULT_OK;
 public class ConversationFragment extends BaseSupportFragment<ConversationPresenter> implements
         MediaButtonsHelper.MediaButtonsHelperCallback,
         ConversationView, SharedPreferences.OnSharedPreferenceChangeListener {
-    protected static final String TAG = ConversationFragment.class.getSimpleName();
+    private static final String TAG = ConversationFragment.class.getSimpleName();
 
     public static final int REQ_ADD_CONTACT = 42;
 
@@ -115,6 +125,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     public static final String KEY_ACCOUNT_ID = BuildConfig.APPLICATION_ID + ".ACCOUNT_ID";
     public static final String KEY_PREFERENCE_PENDING_MESSAGE = "pendingMessage";
     public static final String KEY_PREFERENCE_CONVERSATION_COLOR = "color";
+    public static final String EXTRA_SHOW_MAP = "showMap";
 
     private static final int REQUEST_CODE_FILE_PICKER = 1000;
     private static final int REQUEST_PERMISSION_CAMERA = 1001;
@@ -123,13 +134,14 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     private static final int REQUEST_CODE_CAPTURE_AUDIO = 1004;
     private static final int REQUEST_CODE_CAPTURE_VIDEO = 1005;
 
+    private ServiceConnection locationServiceConnection = null;
+
     private FragConversationBinding binding;
     private MenuItem mAudioCallBtn = null;
     private MenuItem mVideoCallBtn = null;
 
     private View currentBottomView = null;
     private ConversationAdapter mAdapter = null;
-    private NumberAdapter mNumberAdapter = null;
     private int marginPx;
     private int marginPxTotal;
     private final ValueAnimator animation = new ValueAnimator();
@@ -142,7 +154,8 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     private int mSelectedPosition;
 
     private AvatarDrawable mConversationAvatar;
-    private Map<String, AvatarDrawable> mParticipantAvatars = new HashMap();
+    private Map<String, AvatarDrawable> mParticipantAvatars = new HashMap<>();
+    private int mapWidth, mapHeight;
 
     public AvatarDrawable getConversationAvatar(String uri) {
         return mParticipantAvatars.get(uri);
@@ -195,15 +208,22 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     }
 
     private void updateListPadding() {
-        if (currentBottomView != null && currentBottomView.getHeight() != 0)
+        if (currentBottomView != null && currentBottomView.getHeight() != 0) {
             setBottomPadding(binding.histList, currentBottomView.getHeight() + marginPxTotal);
+            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
+            params.bottomMargin = currentBottomView.getHeight() + marginPxTotal;
+            binding.mapCard.setLayoutParams(params);
+        }
     }
 
     @Nullable
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
         injectFragment(((JamiApplication) getActivity().getApplication()).getRingInjectionComponent());
-        marginPx = getResources().getDimensionPixelSize(R.dimen.conversation_message_input_margin);
+        Resources res = getResources();
+        marginPx = res.getDimensionPixelSize(R.dimen.conversation_message_input_margin);
+        mapWidth = res.getDimensionPixelSize(R.dimen.location_sharing_minmap_width);
+        mapHeight = res.getDimensionPixelSize(R.dimen.location_sharing_minmap_height);
         marginPxTotal = marginPx;
 
         binding = FragConversationBinding.inflate(inflater, container, false);
@@ -242,6 +262,14 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
                 .flatMapCompletable(this::sendFile)
                 .doFinally(contentInfo::releasePermission)));
         binding.msgInputTxt.setOnEditorActionListener((v, actionId, event) -> actionSendMsgText(actionId));
+        binding.msgInputTxt.setOnFocusChangeListener((view, hasFocus) -> {
+            if (hasFocus)  {
+                Fragment fragment = getChildFragmentManager().findFragmentById(R.id.mapLayout);
+                if (fragment != null) {
+                    ((LocationSharingFragment) fragment).hideControls();
+                }
+            }
+        });
         binding.msgInputTxt.addTextChangedListener(new TextWatcher() {
             @Override
             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@@ -320,6 +348,13 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         animation.removeAllUpdateListeners();
         binding.histList.setAdapter(null);
         mCompositeDisposable.clear();
+        if (locationServiceConnection != null) {
+            try {
+                requireContext().unbindService(locationServiceConnection);
+            } catch (Exception e) {
+                Log.w(TAG, "Error unbinding service: " + e.getMessage());
+            }
+        }
         super.onDestroyView();
     }
 
@@ -351,6 +386,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         presenter.selectFile();
     }
 
+    @SuppressLint("RestrictedApi")
     public void expandMenu(View v) {
         Context context = requireContext();
         PopupMenu popup = new PopupMenu(context, v);
@@ -394,6 +430,9 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
                 case R.id.conv_send_file:
                     selectFile();
                     break;
+                case R.id.conv_share_location:
+                    shareLocation();
+                    break;
             }
             return false;
         });
@@ -402,6 +441,36 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         menuHelper.show();
     }
 
+    public void shareLocation() {
+        presenter.shareLocation();
+    }
+
+    public void closeLocationSharing(boolean isSharing) {
+        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
+        if (params.width != mapWidth) {
+            params.width = mapWidth;
+            params.height = mapHeight;
+            binding.mapCard.setLayoutParams(params);
+        }
+        if (!isSharing)
+            hideMap();
+    }
+
+    public void openLocationSharing() {
+        binding.conversationLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
+        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) binding.mapCard.getLayoutParams();
+        if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
+            params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+            params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+            binding.mapCard.setLayoutParams(params);
+        }
+    }
+
+    @Override
+    public void startShareLocation(String accountId, String conversationId) {
+        showMap(accountId, conversationId, true);
+    }
+
     /**
      * Used to update with the past adapter position when a long click was registered
      * @param position
@@ -410,6 +479,42 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         mSelectedPosition = position;
     }
 
+    @Override
+    public void showMap(String accountId, String contactId, boolean open)  {
+        if (binding.mapCard.getVisibility() == View.GONE) {
+            Log.w(TAG, "showMap " + accountId + " " + contactId);
+
+            FragmentManager fragmentManager = getChildFragmentManager();
+            LocationSharingFragment fragment = LocationSharingFragment.newInstance(accountId, contactId, open);
+            fragmentManager.beginTransaction()
+                    .add(R.id.mapLayout, fragment, "map")
+                    .commit();
+            binding.mapCard.setVisibility(View.VISIBLE);
+        }
+        if (open) {
+            Fragment fragment = getChildFragmentManager().findFragmentById(R.id.mapLayout);
+            if (fragment != null) {
+                ((LocationSharingFragment) fragment).showControls();
+            }
+        }
+    }
+
+    @Override
+    public void hideMap() {
+        if (binding.mapCard.getVisibility() != View.GONE) {
+            binding.mapCard.setVisibility(View.GONE);
+
+            FragmentManager fragmentManager = getChildFragmentManager();
+            Fragment fragment = fragmentManager.findFragmentById(R.id.mapLayout);
+
+            if (fragment != null) {
+                fragmentManager.beginTransaction()
+                        .remove(fragment)
+                        .commit();
+            }
+        }
+    }
+
     public void takePicture() {
         if (!presenter.getDeviceRuntimeService().hasVideoPermission()) {
             requestPermissions(new String[]{Manifest.permission.CAMERA}, JamiApplication.PERMISSIONS_REQUEST);
@@ -692,6 +797,35 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         } catch (Exception e) {
             Log.e(TAG, "Can't load conversation preferences");
         }
+
+        if (locationServiceConnection == null) {
+            locationServiceConnection = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    Log.w(TAG, "onServiceConnected");
+                    LocationSharingService.LocalBinder binder = (LocationSharingService.LocalBinder) service;
+                    LocationSharingService locationService = binder.getService();
+                    ConversationPath path = new ConversationPath(presenter.getPath());
+                    if (locationService.isSharing(path)) {
+                        showMap(accountId, contactUri.getUri(), false);
+                    }
+                    try {
+                        requireContext().unbindService(locationServiceConnection);
+                    } catch (Exception e) {
+                        Log.w(TAG, "Error unbinding service", e);
+                    }
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    Log.w(TAG, "onServiceDisconnected");
+                    locationServiceConnection = null;
+                }
+            };
+
+            Log.w(TAG, "bindService");
+            requireContext().bindService(new Intent(requireContext(), LocationSharingService.class), locationServiceConnection, 0);
+        }
     }
 
     @Override
@@ -733,10 +867,8 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     @Override
     public void displayNumberSpinner(final Conversation conversation, final Uri number) {
         binding.numberSelector.setVisibility(View.VISIBLE);
-        mNumberAdapter = new NumberAdapter(getActivity(),
-                conversation.getContact(),
-                false);
-        binding.numberSelector.setAdapter(mNumberAdapter);
+        binding.numberSelector.setAdapter(new NumberAdapter(getActivity(),
+                conversation.getContact(), false));
         binding.numberSelector.setSelection(getIndex(binding.numberSelector, number));
     }
 
@@ -863,7 +995,7 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     }
 
     @Override
-    public void onPrepareOptionsMenu(Menu menu) {
+    public void onPrepareOptionsMenu(@NonNull Menu menu) {
         super.onPrepareOptionsMenu(menu);
         boolean visible = binding.cvMessageInput.getVisibility() == View.VISIBLE;
         if (mAudioCallBtn != null)
@@ -935,21 +1067,31 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
     }
 
     public void handleShareIntent(Intent intent) {
-        String type = intent.getType();
-        if (type == null) {
-            Log.w(TAG, "Can't share with no type");
-            return;
-        }
-        if (type.startsWith("text/plain")) {
-            binding.msgInputTxt.setText(intent.getStringExtra(Intent.EXTRA_TEXT));
-        } else {
-            android.net.Uri uri = intent.getData();
-            ClipData clip = intent.getClipData();
-            if (uri == null && clip != null && clip.getItemCount() > 0)
-                uri = clip.getItemAt(0).getUri();
-            if (uri == null)
+        Log.w(TAG, "handleShareIntent " + intent);
+
+        String action = intent.getAction();
+        if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+            String type = intent.getType();
+            if (type == null) {
+                Log.w(TAG, "Can't share with no type");
                 return;
-            startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri).flatMapCompletable(this::sendFile));
+            }
+            if (type.startsWith("text/plain")) {
+                binding.msgInputTxt.setText(intent.getStringExtra(Intent.EXTRA_TEXT));
+            } else {
+                android.net.Uri uri = intent.getData();
+                ClipData clip = intent.getClipData();
+                if (uri == null && clip != null && clip.getItemCount() > 0)
+                    uri = clip.getItemAt(0).getUri();
+                if (uri == null)
+                    return;
+                startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri).flatMapCompletable(this::sendFile));
+            }
+        } else if (Intent.ACTION_VIEW.equals(action)) {
+            ConversationPath path = ConversationPath.fromIntent(intent);
+            if (path != null && intent.getBooleanExtra(EXTRA_SHOW_MAP, false)) {
+                shareLocation();
+            }
         }
     }
 
@@ -967,7 +1109,6 @@ public class ConversationFragment extends BaseSupportFragment<ConversationPresen
         //Use Android Storage File Access to download the file
         Intent downloadFileIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
         downloadFileIntent.setType(AndroidFileUtils.getMimeTypeFromExtension(file.getExtension()));
-
         downloadFileIntent.addCategory(Intent.CATEGORY_OPENABLE);
         downloadFileIntent.putExtra(Intent.EXTRA_TITLE,file.getDisplayName());
 
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..b34d0bb5cc85a60b0da1dd7ea5689dc104756756
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/fragments/LocationSharingFragment.java
@@ -0,0 +1,617 @@
+/*
+ *  Copyright (C) 2004-2020 Savoir-faire Linux Inc.
+ *
+ *  Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.fragments;
+
+import android.Manifest;
+import android.animation.LayoutTransition;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.BitmapDrawable;
+import android.icu.text.MeasureFormat;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.location.Location;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
+
+import android.os.IBinder;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.android.material.chip.Chip;
+import com.google.android.material.chip.ChipGroup;
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
+
+import org.osmdroid.config.Configuration;
+import org.osmdroid.config.IConfigurationProvider;
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
+import org.osmdroid.util.BoundingBox;
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.CustomZoomButtonsController;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.Marker;
+import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer;
+import org.osmdroid.views.overlay.mylocation.IMyLocationProvider;
+import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import cx.ring.R;
+import cx.ring.application.JamiApplication;
+import cx.ring.contacts.AvatarFactory;
+import cx.ring.facades.ConversationFacade;
+import cx.ring.model.Account;
+import cx.ring.model.CallContact;
+import cx.ring.model.Uri;
+import cx.ring.services.LocationSharingService;
+import cx.ring.utils.ConversationPath;
+import cx.ring.utils.TouchClickListener;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.subjects.BehaviorSubject;
+import io.reactivex.subjects.Subject;
+
+public class LocationSharingFragment extends Fragment {
+    private static final String TAG = LocationSharingFragment.class.getSimpleName();
+    private static final int REQUEST_CODE_LOCATION = 47892;
+    private static final String KEY_SHOW_CONTROLS = "showControls";
+
+    private final CompositeDisposable mDisposableBag = new CompositeDisposable();
+    private final CompositeDisposable mServiceDisposableBag = new CompositeDisposable();
+
+    enum MapState {NONE, MINI, FULL}
+
+    @Inject
+    ConversationFacade mConversationFacade;
+
+    private ConversationPath mPath;
+    private CallContact mContact;
+
+    private final Subject<Boolean> mShowControlsSubject = BehaviorSubject.create();
+    private final Subject<Boolean> mIsSharingSubject = BehaviorSubject.create();
+    private final Subject<Boolean> mIsContactSharingSubject = BehaviorSubject.create();
+    private final Observable<MapState> mShowMapSubject = Observable.combineLatest(
+            mShowControlsSubject,
+            mIsSharingSubject,
+            mIsContactSharingSubject,
+            (showControls, isSharing, isContactSharing) -> showControls
+                    ? MapState.FULL
+                    : ((isSharing || isContactSharing) ? MapState.MINI : MapState.NONE))
+            .distinctUntilChanged();
+
+    private int bubbleSize;
+
+    private Toolbar mToolbar;
+    private ViewGroup mSnippetGroup;
+    private TextView mSnippet;
+    private ImageView mSnippetShadow;
+    private ViewGroup mShareControls;
+    private ViewGroup mShareControlsMini;
+    private ExtendedFloatingActionButton mShareButton;
+    private Chip mStopShareButton;
+    private ChipGroup mShareTimeGroup;
+    private Chip mTimeRemaining;
+    private MapView mMap = null;
+    private MyLocationNewOverlay overlay;
+    private Marker marker;
+    private BoundingBox lastBoundingBox = null;
+    private boolean trackAll = true;
+    private Integer mStartSharingPending = null;
+
+    private LocationSharingService mService = null;
+    private boolean mBound = false;
+
+    public LocationSharingFragment() {
+        super(R.layout.frag_location_sharing);
+    }
+
+    public static LocationSharingFragment newInstance(String accountId, String conversationId, boolean showControls) {
+        LocationSharingFragment fragment = new LocationSharingFragment();
+        Bundle args = ConversationPath.toBundle(accountId, conversationId);
+        args.putBoolean(KEY_SHOW_CONTROLS, showControls);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        ((JamiApplication) requireActivity().getApplication()).getRingInjectionComponent().inject(this);
+        setRetainInstance(true);
+
+        Bundle args = getArguments();
+        if (args != null) {
+            mPath = ConversationPath.fromBundle(args);
+            mShowControlsSubject.onNext(args.getBoolean(KEY_SHOW_CONTROLS, true));
+        }
+
+        Context ctx = requireContext();
+        IConfigurationProvider configuration = Configuration.getInstance();
+        configuration.load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
+        configuration.setOsmdroidBasePath(new File(ctx.getCacheDir(), "osm"));
+        configuration.setMapViewHardwareAccelerated(true);
+
+        bubbleSize = ctx.getResources().getDimensionPixelSize(R.dimen.location_sharing_avatar_size);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private static CharSequence formatDuration(long millis, MeasureFormat.FormatWidth width) {
+        final MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(), width);
+        if (millis >= DateUtils.HOUR_IN_MILLIS) {
+            final int hours = (int) ((millis + DateUtils.HOUR_IN_MILLIS/2) / DateUtils.HOUR_IN_MILLIS);
+            return formatter.format(new Measure(hours, MeasureUnit.HOUR));
+        } else if (millis >= DateUtils.MINUTE_IN_MILLIS) {
+            final int minutes = (int) ((millis + DateUtils.MINUTE_IN_MILLIS/2) / DateUtils.MINUTE_IN_MILLIS);
+            return formatter.format(new Measure(minutes, MeasureUnit.MINUTE));
+        } else {
+            final int seconds = (int) ((millis + DateUtils.SECOND_IN_MILLIS/2) / DateUtils.SECOND_IN_MILLIS);
+            return formatter.format(new Measure(seconds, MeasureUnit.SECOND));
+        }
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        mToolbar = view.findViewById(R.id.locshare_toolbar);
+        mSnippetGroup = view.findViewById(R.id.locshare_snipet);
+        mSnippet = view.findViewById(R.id.locshare_snipet_txt);
+        mSnippetShadow = view.findViewById(R.id.locshare_snipet_txt_shadow);
+        mMap = view.findViewById(R.id.map);
+        mShareControls = view.findViewById(R.id.shareControls);
+        mShareControlsMini = view.findViewById(R.id.shareControlsMini);
+        mShareButton = view.findViewById(R.id.btn_share_location);
+        mStopShareButton = view.findViewById(R.id.location_share_stop);
+        mTimeRemaining = view.findViewById(R.id.location_share_time_remaining);
+        mShareTimeGroup = view.findViewById(R.id.location_share_time_group);
+        Chip chip_1h = view.findViewById(R.id.location_share_time_1h);
+        Chip chip_10m = view.findViewById(R.id.location_share_time_10m);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            chip_1h.setText(formatDuration( DateUtils.HOUR_IN_MILLIS, MeasureFormat.FormatWidth.WIDE));
+            chip_10m.setText(formatDuration( 10 * DateUtils.MINUTE_IN_MILLIS, MeasureFormat.FormatWidth.WIDE));
+        }
+
+        View locateView = view.findViewById(R.id.btn_center_position);
+        locateView.setOnClickListener(v -> {
+            if (overlay != null) {
+                trackAll = true;
+                if (lastBoundingBox != null)
+                    mMap.zoomToBoundingBox(lastBoundingBox, true);
+                else
+                    overlay.enableFollowLocation();
+            }
+        });
+        mShareTimeGroup.setOnCheckedChangeListener((group, id) -> {
+            if (id == View.NO_ID)
+                group.check(R.id.location_share_time_1h);
+        });
+        mToolbar.setNavigationOnClickListener(v -> mShowControlsSubject.onNext(false));
+        mStopShareButton.setOnClickListener(v -> stopSharing());
+
+        mMap.setTileSource(TileSourceFactory.MAPNIK);
+        mMap.setHorizontalMapRepetitionEnabled(false);
+        mMap.setTilesScaledToDpi(true);
+        mMap.setMapOrientation(0, false);
+        mMap.setMinZoomLevel(1d);
+        mMap.setMaxZoomLevel(19.d);
+        mMap.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
+        mMap.getController().setZoom(14.0);
+        ((ViewGroup)view).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
+    }
+
+    private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
+        @Override
+        public void handleOnBackPressed() {
+            mShowControlsSubject.onNext(false);
+        }
+    };
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mShowControlsSubject.onComplete();
+        mIsSharingSubject.onComplete();
+        mIsContactSharingSubject.onComplete();
+    }
+
+    public void onResume() {
+        super.onResume();
+        mMap.onResume();
+        if (overlay != null)
+            overlay.enableMyLocation();
+    }
+
+    public void onPause(){
+        super.onPause();
+        mMap.onPause();
+        if (overlay != null)
+            overlay.disableMyLocation();
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        if (requestCode == REQUEST_CODE_LOCATION) {
+            boolean granted = false;
+            for (int result : grantResults)
+                granted |= (result == PackageManager.PERMISSION_GRANTED);
+            if (granted) {
+                startService();
+            } else {
+                mIsSharingSubject.onNext(false);
+                mShowControlsSubject.onNext(false);
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mDisposableBag.add(mServiceDisposableBag);
+        mDisposableBag.add(mShowControlsSubject.subscribe(this::setShowControls));
+        mDisposableBag.add(mIsSharingSubject.subscribe(this::setIsSharing));
+        mDisposableBag.add(mShowMapSubject.subscribe(state -> {
+            Fragment p = getParentFragment();
+            if (p instanceof ConversationFragment) {
+                ConversationFragment parent = (ConversationFragment) p;
+                if (state == MapState.FULL)
+                    parent.openLocationSharing();
+                else
+                    parent.closeLocationSharing(state == MapState.MINI);
+            }
+        }));
+        mDisposableBag.add(mIsContactSharingSubject
+                .distinctUntilChanged()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(sharing -> {
+                    if (sharing) {
+                        String sharingString = getString(R.string.location_share_contact, mContact.getDisplayName());
+                        mToolbar.setSubtitle(sharingString);
+                        mSnippet.setVisibility(View.VISIBLE);
+                        mSnippetShadow.setVisibility(View.VISIBLE);
+                        mSnippet.setText(sharingString);
+                    } else {
+                        mToolbar.setSubtitle(null);
+                        mSnippet.setVisibility(View.GONE);
+                        mSnippetShadow.setVisibility(View.GONE);
+                        mSnippet.setText(null);
+                    }
+                }));
+
+        final Uri contactUri = new Uri(mPath.getContactId());
+
+        mDisposableBag.add(mConversationFacade
+                .getAccountSubject(mPath.getAccountId())
+                .flatMapObservable(account -> account.getLocationsUpdates()
+                        .map(locations -> {
+                            List<Observable<LocationViewModel>> r = new ArrayList<>(locations.size());
+                            boolean isContactSharing = false;
+                            for (Map.Entry<CallContact, Observable<Account.ContactLocation>> l : locations.entrySet()) {
+                                if (l.getKey() == account.getContactFromCache(contactUri)) {
+                                    isContactSharing = true;
+                                    mContact = l.getKey();
+                                }
+                                r.add(l.getValue().map(cl -> new LocationViewModel(l.getKey(), cl)));
+                            }
+                            mIsContactSharingSubject.onNext(isContactSharing);
+                            return r;
+                        }))
+                .flatMap(locations -> Observable.combineLatest(locations, locsArray -> {
+                    List<LocationViewModel> list = new ArrayList<>(locsArray.length);
+                    for (Object vm : locsArray)
+                        list.add((LocationViewModel)vm);
+                    return list;
+                }))
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(locations -> {
+                    Context context = getContext();
+                    if (context != null) {
+                        mMap.getOverlays().clear();
+                        if (overlay != null)
+                            mMap.getOverlays().add(overlay);
+                        if (marker != null)
+                            mMap.getOverlays().add(marker);
+
+                        List<GeoPoint> geoLocations =new ArrayList<>(locations.size() + 1);
+                        GeoPoint myLoc = overlay == null ? null : overlay.getMyLocation();
+                        if (myLoc != null) {
+                            geoLocations.add(myLoc);
+                        }
+
+                        for (LocationViewModel vm : locations) {
+                            Marker m = new Marker(mMap);
+                            GeoPoint position = new GeoPoint(vm.location.latitude, vm.location.longitude);
+                            m.setInfoWindow(null);
+                            m.setPosition(position);
+                            m.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
+                            geoLocations.add(position);
+                            mDisposableBag.add(AvatarFactory.getBitmapAvatar(context, vm.contact, bubbleSize, false).subscribe(avatar ->  {
+                                BitmapDrawable bd = new BitmapDrawable(context.getResources(), avatar);
+                                m.setIcon(bd);
+                                m.setInfoWindow(null);
+                                mMap.getOverlays().add(m);
+                            }));
+                        }
+
+                        if (trackAll) {
+                            if (geoLocations.size() == 1) {
+                                lastBoundingBox = null;
+                                mMap.getController().animateTo(geoLocations.get(0));
+                            } else {
+                                BoundingBox bb = BoundingBox.fromGeoPointsSafe(geoLocations);
+                                bb = bb.increaseByScale(1.5f);
+                                lastBoundingBox = bb;
+                                mMap.zoomToBoundingBox(bb, true);
+                            }
+                        }
+                    }
+                }, e -> Log.w(TAG, "Error updating contact position", e))
+        );
+
+        Context ctx = requireContext();
+        if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
+                && ActivityCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+            mIsSharingSubject.onNext(false);
+            mDisposableBag.add(mShowControlsSubject
+                    .firstElement()
+                    .subscribe(showControls -> {
+                        if (showControls) {
+                            requestPermissions(new String[]{ Manifest.permission.ACCESS_FINE_LOCATION }, REQUEST_CODE_LOCATION);
+                        }
+                    }));
+        } else {
+            startService();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mBound) {
+            mServiceDisposableBag.clear();
+            requireContext().unbindService(mConnection);
+            mBound = false;
+        }
+        mDisposableBag.clear();
+    }
+
+    private void startService() {
+        Context ctx = requireContext();
+        ctx.bindService(new Intent(ctx, LocationSharingService.class), mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+
+    void showControls() {
+        mShowControlsSubject.onNext(true);
+    }
+
+    void hideControls() {
+        mShowControlsSubject.onNext(false);
+    }
+
+    private void setShowControls(boolean show) {
+        if (show) {
+            onBackPressedCallback.setEnabled(true);
+            mSnippetGroup.setVisibility(View.GONE);
+            mShareControlsMini.setVisibility(View.GONE);
+            mShareControlsMini.postDelayed(() -> {
+                mShareControlsMini.setVisibility(View.GONE);
+                mSnippetGroup.setVisibility(View.GONE);
+            }, 300);
+            mShareControls.setVisibility(View.VISIBLE);
+            mToolbar.setVisibility(View.VISIBLE);
+            mMap.setOnTouchListener(null);
+            mMap.setMultiTouchControls(true);
+        } else {
+            onBackPressedCallback.setEnabled(false);
+            mShareControls.setVisibility(View.GONE);
+            mShareControlsMini.postDelayed(() -> {
+                mShareControlsMini.setVisibility(View.VISIBLE);
+                mSnippetGroup.setVisibility(View.VISIBLE);
+            }, 300);
+            mToolbar.setVisibility(View.GONE);
+            mMap.setMultiTouchControls(false);
+            mMap.setOnTouchListener(new TouchClickListener(mMap.getContext(), v -> mShowControlsSubject.onNext(true)));
+        }
+    }
+
+    static class RxLocationListener implements IMyLocationProvider {
+        private final CompositeDisposable mDisposableBag = new CompositeDisposable();
+        private Observable<Location> mLocation;
+
+        RxLocationListener(Observable<Location> location) {
+            mLocation = location;
+        }
+
+        @Override
+        public boolean startLocationProvider(IMyLocationConsumer myLocationConsumer) {
+            mDisposableBag.add(mLocation.subscribe(loc -> myLocationConsumer.onLocationChanged(loc, this)));
+            return false;
+        }
+
+        @Override
+        public void stopLocationProvider() {
+            mDisposableBag.clear();
+        }
+
+        @Override
+        public Location getLastKnownLocation() {
+            return mLocation.blockingFirst();
+        }
+
+        @Override
+        public void destroy() {
+            mDisposableBag.dispose();
+            mLocation = null;
+        }
+    }
+
+    static class LocationViewModel {
+        CallContact contact;
+        Account.ContactLocation location;
+        LocationViewModel(CallContact c, Account.ContactLocation cl) {
+            contact = c;
+            location = cl;
+        }
+    }
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.w(TAG, "onServiceConnected");
+            LocationSharingService.LocalBinder binder = (LocationSharingService.LocalBinder) service;
+            mService = binder.getService();
+            mBound = true;
+
+            if (marker == null) {
+                marker = new Marker(mMap);
+                marker.setInfoWindow(null);
+                marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER);
+                mServiceDisposableBag.add(mConversationFacade
+                        .getAccountSubject(mPath.getAccountId())
+                        .flatMap(account -> AvatarFactory.getBitmapAvatar(requireContext(), account, bubbleSize))
+                        .subscribe(avatar -> {
+                            marker.setIcon(new BitmapDrawable(requireContext().getResources(), avatar));
+                            mMap.getOverlays().add(marker);
+                        }));
+            }
+
+            mServiceDisposableBag.add(mService.getContactSharing()
+                    .subscribe(location -> mIsSharingSubject.onNext(location.contains(mPath))));
+            mServiceDisposableBag.add(mService.getMyLocation()
+                    .subscribe(location -> marker.setPosition(new GeoPoint(location))));
+            mServiceDisposableBag.add(mService.getMyLocation()
+                    .firstElement()
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(location  -> {
+                        // start map on first location
+                        mMap.setExpectedCenter(new GeoPoint(location));
+                        overlay = new MyLocationNewOverlay(new RxLocationListener(mService.getMyLocation()), mMap);
+                        overlay.enableMyLocation();
+                        mMap.getOverlays().add(overlay);
+                    }));
+
+            if (mStartSharingPending != null) {
+                Integer pending = mStartSharingPending;
+                mStartSharingPending = null;
+                startSharing(pending);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mBound = false;
+            mService = null;
+            mServiceDisposableBag.clear();
+        }
+    };
+
+    private int getSelectedDuration() {
+        switch (mShareTimeGroup.getCheckedChipId()) {
+            case R.id.location_share_time_10m:
+                return 10 * 60;
+            case R.id.location_share_time_1h:
+            default:
+                return 60 * 60;
+        }
+    }
+
+    private void setIsSharing(boolean sharing) {
+        if (sharing) {
+            mShareButton.setBackgroundColor(ContextCompat.getColor(mShareButton.getContext(), R.color.design_default_color_error));
+            mShareButton.setText(R.string.location_share_action_stop);
+            mShareButton.setOnClickListener(v -> stopSharing());
+            mShareTimeGroup.setVisibility(View.GONE);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                mTimeRemaining.setVisibility(View.VISIBLE);
+                mDisposableBag.add(mService.getContactSharingExpiration(mPath)
+                        .subscribe(l -> mTimeRemaining.setText(formatDuration(l, MeasureFormat.FormatWidth.SHORT))));
+            }
+            mStopShareButton.setVisibility(View.VISIBLE);
+            requireView().post(this::hideControls);
+        } else {
+            mShareButton.setBackgroundColor(ContextCompat.getColor(mShareButton.getContext(), R.color.colorSecondary));
+            mShareButton.setText(R.string.location_share_action_start);
+            mShareButton.setOnClickListener(v -> startSharing(getSelectedDuration()));
+            mTimeRemaining.setVisibility(View.GONE);
+            mShareTimeGroup.setVisibility(View.VISIBLE);
+            mStopShareButton.setVisibility(View.GONE);
+        }
+    }
+
+    private void startSharing(int durationSec) {
+        Context ctx = requireContext();
+        try {
+            if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
+                    && ActivityCompat.checkSelfPermission(ctx, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+                mStartSharingPending = durationSec;
+                requestPermissions(new String[]{ Manifest.permission.ACCESS_FINE_LOCATION }, REQUEST_CODE_LOCATION);
+            } else {
+                Intent intent = new Intent(LocationSharingService.ACTION_START, mPath.toUri(), ctx, LocationSharingService.class);
+                intent.putExtra(LocationSharingService.EXTRA_SHARING_DURATION, durationSec);
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                    ctx.startForegroundService(intent);
+                } else {
+                    ctx.startService(intent);
+                }
+            }
+        } catch (Exception e) {
+            Toast.makeText(ctx, "Error starting location sharing: " + e.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    private void stopSharing() {
+        Context ctx = requireContext();
+        try {
+            Intent intent = new Intent(LocationSharingService.ACTION_STOP, mPath.toUri(), ctx, LocationSharingService.class);
+            ctx.startService(intent);
+        } catch (Exception e) {
+            Log.w(TAG, "Error stopping location sharing", e);
+        }
+    }
+}
diff --git a/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java b/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..f504f81e89dffdef513f0bc3b89bffb1944671b6
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/services/LocationSharingService.java
@@ -0,0 +1,387 @@
+/*
+ *  Copyright (C) 2004-2020 Savoir-faire Linux Inc.
+ *
+ *  Authors: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.services;
+
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+
+import org.json.JSONObject;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+import cx.ring.R;
+import cx.ring.application.JamiApplication;
+import cx.ring.client.ConversationActivity;
+import cx.ring.daemon.Blob;
+import cx.ring.daemon.Ringservice;
+import cx.ring.daemon.StringMap;
+import cx.ring.facades.ConversationFacade;
+import cx.ring.fragments.ConversationFragment;
+import cx.ring.model.Uri;
+import cx.ring.utils.ConversationPath;
+import io.reactivex.Observable;
+import io.reactivex.ObservableSource;
+import io.reactivex.Observer;
+import io.reactivex.Single;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subjects.BehaviorSubject;
+import io.reactivex.subjects.Subject;
+
+public class LocationSharingService extends Service implements LocationListener {
+    private static final String TAG = "LocationSharingService";
+
+    public static final int NOTIF_SYNC_SERVICE_ID = 931801;
+
+    public static final String ACTION_START = "startSharing";
+    public static final String ACTION_STOP = "stopSharing";
+    public static final String EXTRA_SHARING_DURATION = "locationShareDuration";
+
+    public static final String PREFERENCES_LOCATION = "location";
+    public static final String PREFERENCES_KEY_POS_LONG = "lastPosLongitude";
+    public static final String PREFERENCES_KEY_POS_LAT = "lastPosLatitude";
+    public static final int SHARE_DURATION_SEC = 60 * 5;
+
+    @Inject
+    ConversationFacade mConversationFacade;
+
+    private final Random mRandom = new Random();
+    private final IBinder binder = new LocalBinder();
+    private boolean started = false;
+
+    private LocationManager mLocationManager;
+    private NotificationManager mNoticationManager;
+    private SharedPreferences mPreferences;
+    private Handler mHandler;
+
+    private final Subject<Location> mMyLocationSubject = BehaviorSubject.create();
+    private final Map<ConversationPath, Date> contactLocationShare = new HashMap<>();
+    private final Subject<Set<ConversationPath>> mContactSharingSubject = BehaviorSubject.createDefault(contactLocationShare.keySet());
+
+    private final CompositeDisposable mDisposableBag = new CompositeDisposable();
+
+    public LocationSharingService() {
+    }
+
+    public Observable<Location> getMyLocation() {
+        return mMyLocationSubject;
+    }
+
+    public Observable<Set<ConversationPath>> getContactSharing() {
+        return mContactSharingSubject;
+    }
+
+    public Observable<Long> getContactSharingExpiration(ConversationPath path) {
+        return Observable.timer(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
+                .startWith(0L)
+                .repeat()
+                .map(i -> contactLocationShare.get(path).getTime() - SystemClock.elapsedRealtime())
+                .onErrorResumeNext((ObservableSource<Long>) Observer::onComplete);
+    }
+
+    @Override
+    public void onCreate() {
+        ((JamiApplication) getApplication()).getRingInjectionComponent().inject(this);
+
+        mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+        mNoticationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+        mPreferences = getSharedPreferences(PREFERENCES_LOCATION, Context.MODE_PRIVATE);
+        mHandler = new Handler(getMainLooper());
+        String posLongitude = mPreferences.getString(PREFERENCES_KEY_POS_LONG, null);
+        String posLatitude = mPreferences.getString(PREFERENCES_KEY_POS_LAT, null);
+        if (posLatitude != null && posLongitude != null) {
+            try {
+                Location location = new Location("cache");
+                location.setLatitude(Double.parseDouble(posLatitude));
+                location.setLongitude(Double.parseDouble(posLongitude));
+                mMyLocationSubject.onNext(location);
+            } catch (Exception e) {
+                Log.w(TAG, "Can't load last location", e);
+            }
+        }
+        if (mLocationManager != null) {
+            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
+                    || ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
+                try {
+                    Criteria c = new Criteria();
+                    c.setAccuracy(Criteria.ACCURACY_FINE);
+                    mLocationManager.requestLocationUpdates(0, 0.f, c, this, null);
+                } catch (Exception e) {
+                    Log.e(TAG, "Can't start location tracking", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.w(TAG, "onStartCommand " + intent);
+        String action = intent.getAction();
+        ConversationPath path = ConversationPath.fromIntent(intent);
+        long now = SystemClock.elapsedRealtime();
+
+        if (ACTION_START.equals(action)) {
+            int duration = intent.getIntExtra(EXTRA_SHARING_DURATION, SHARE_DURATION_SEC);
+            long expiration = now + (duration * 1000L);
+            if (contactLocationShare.put(path, new Date(expiration)) == null) {
+                mContactSharingSubject.onNext(contactLocationShare.keySet());
+            }
+            mHandler.postAtTime(this::refreshSharing, expiration);
+
+            if (!started) {
+                started = true;
+                mDisposableBag.add(getNotification(now)
+                        .subscribe(notification -> {
+                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+                                startForeground(NOTIF_SYNC_SERVICE_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
+                            else
+                                startForeground(NOTIF_SYNC_SERVICE_ID, notification);
+
+                            mHandler.postAtTime(this::refreshNotificationTimer, now + 30 * 1000);
+                            JamiApplication.getInstance().startDaemon();
+                        }));
+                mDisposableBag.add(mMyLocationSubject
+                        .throttleLatest(10, TimeUnit.SECONDS)
+                        .map(location -> {
+                            JSONObject out = new JSONObject();
+                            out.put("lat", location.getLatitude());
+                            out.put("long", location.getLongitude());
+                            out.put("alt", location.getAltitude());
+                            out.put("time",location.getElapsedRealtimeNanos()/1000000L);
+                            float bearing = location.getBearing();
+                            if (bearing != 0.f)
+                                out.put("bearing", bearing);
+                            float speed = location.getSpeed();
+                            if (speed != 0.f)
+                                out.put("speed", speed);
+                            return out;
+                        })
+                        .subscribe(location -> {
+                            Log.w(TAG, "location send " + location + " to " + contactLocationShare.size());
+                            StringMap msgs = new StringMap();
+                            msgs.setRaw(CallService.MIME_GEOLOCATION, Blob.fromString(location.toString()));
+                            for (ConversationPath p : contactLocationShare.keySet())  {
+                                Ringservice.sendAccountTextMessage(p.getAccountId(), p.getContactId(), msgs);
+                            }
+                        }));
+            } else {
+                mDisposableBag.add(getNotification(now)
+                        .subscribe(notification -> mNoticationManager.notify(NOTIF_SYNC_SERVICE_ID, notification)));
+            }
+        }
+        else if (ACTION_STOP.equals(action)) {
+            if (path == null)
+                contactLocationShare.clear();
+            else
+                contactLocationShare.remove(path);
+
+            mContactSharingSubject.onNext(contactLocationShare.keySet());
+
+            if (contactLocationShare.isEmpty()) {
+                Log.w(TAG, "stopping sharing " + intent);
+                mDisposableBag.clear();
+                stopForeground(true);
+                stopSelf();
+                started = false;
+            } else {
+                mDisposableBag.add(getNotification(now)
+                        .subscribe(notification -> mNoticationManager.notify(NOTIF_SYNC_SERVICE_ID, notification)));
+            }
+        }
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.w(TAG, "onDestroy");
+        if (mLocationManager !=  null) {
+            mLocationManager.removeUpdates(this);
+        }
+        mMyLocationSubject.onComplete();
+        mContactSharingSubject.onComplete();
+        mDisposableBag.dispose();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return binder;
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        return true;
+    }
+
+    @Override
+    public void onLocationChanged(Location location) {
+        // Log.w(TAG, "onLocationChanged " + location.toString());
+        mMyLocationSubject.onNext(location);
+        mPreferences.edit()
+                .putString(PREFERENCES_KEY_POS_LAT, Double.toString(location.getLatitude()))
+                .putString(PREFERENCES_KEY_POS_LONG, Double.toString(location.getLongitude()))
+                .apply();
+    }
+
+    @Override
+    public void onStatusChanged(String provider, int status, Bundle extras) {}
+
+    @Override
+    public void onProviderEnabled(String provider) {
+    }
+
+    @Override
+    public void onProviderDisabled(String provider) {
+    }
+
+    @NonNull
+    private Single<Notification> getNotification(long now) {
+        int contactCount = contactLocationShare.size();
+        ConversationPath firsPath = contactLocationShare.keySet().iterator().next();
+        Date largest = null;
+        for (Date d : contactLocationShare.values())
+            if (largest == null || d.after(largest))
+                largest = d;
+        final long largestDate = largest == null ? now : largest.getTime();
+        // Log.w(TAG, "getNotification " + firsPath.getContactId());
+
+        return mConversationFacade.getAccountSubject(firsPath.getAccountId())
+                .map(account -> account.getContactFromCache(new Uri(firsPath.getContactId())))
+                .map(contact -> {
+                    String title;
+                    final Intent stopIntent = new Intent(ACTION_STOP).setClass(getApplicationContext(), LocationSharingService.class);
+                    final Intent contentIntent = new Intent(Intent.ACTION_VIEW, firsPath.toUri(), getApplicationContext(), ConversationActivity.class)
+                            .putExtra(ConversationFragment.EXTRA_SHOW_MAP, true)
+                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+                    if (contactCount == 1) {
+                        stopIntent.setData(firsPath.toUri());
+                        title = getString(R.string.notif_location_title, contact.getDisplayName());
+                    } else {
+                        title = getString(R.string.notif_location_multi_title, contactCount);
+                    }
+                    String subtitle = getString(R.string.notif_location_remaining, (int)Math.ceil((largestDate - now)/(double)(1000 * 60)));
+
+                    return new NotificationCompat.Builder(this, NotificationServiceImpl.NOTIF_CHANNEL_SYNC)
+                            .setContentTitle(title)
+                            .setContentText(subtitle)
+                            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+                            .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
+                            .setAutoCancel(false)
+                            .setOngoing(false)
+                            .setVibrate(null)
+                            .setColorized(true)
+                            .setColor(getResources().getColor(R.color.color_primary_dark))
+                            .setSmallIcon(R.drawable.ic_ring_logo_white)
+                            .setCategory(NotificationCompat.CATEGORY_PROGRESS)
+                            .setOnlyAlertOnce(true)
+                            .setDeleteIntent(PendingIntent.getService(getApplicationContext(), mRandom.nextInt(), stopIntent, 0))
+                            .setContentIntent(PendingIntent.getActivity(getApplicationContext(), mRandom.nextInt(), contentIntent, 0))
+                            .addAction(R.drawable.baseline_location_disabled_24,
+                                    getText(R.string.notif_location_action_stop),
+                                    PendingIntent.getService(
+                                            getApplicationContext(),
+                                            0,
+                                            stopIntent,
+                                            PendingIntent.FLAG_ONE_SHOT))
+                            .build();
+                })
+                .subscribeOn(Schedulers.computation())
+                .observeOn(AndroidSchedulers.mainThread());
+    }
+
+    private void refreshSharing() {
+        if (!started)
+            return;
+
+        boolean changed = false;
+        final Date now = new Date(SystemClock.uptimeMillis());
+        Iterator<Map.Entry<ConversationPath, Date>> it = contactLocationShare.entrySet().iterator();
+        while (it.hasNext())  {
+            Map.Entry<ConversationPath, Date> e = it.next();
+            if (e.getValue().before(now)) {
+                changed = true;
+                it.remove();
+            }
+        }
+
+        if (changed)
+            mContactSharingSubject.onNext(contactLocationShare.keySet());
+
+        if (contactLocationShare.isEmpty()) {
+            mDisposableBag.clear();
+            stopForeground(true);
+            stopSelf();
+            started = false;
+        } else if (changed) {
+            mDisposableBag.add(getNotification(now.getTime())
+                    .subscribe(notification -> mNoticationManager.notify(NOTIF_SYNC_SERVICE_ID, notification)));
+        }
+    }
+
+    private void refreshNotificationTimer() {
+        if (!started)
+            return;
+        long now = SystemClock.uptimeMillis();
+        mDisposableBag.add(getNotification(now)
+                .subscribe(notification -> mNoticationManager.notify(NOTIF_SYNC_SERVICE_ID, notification)));
+        mHandler.postAtTime(this::refreshNotificationTimer, now + (30 * 1000));
+    }
+
+    public boolean isSharing(ConversationPath path) {
+        return contactLocationShare.get(path) != null;
+    }
+
+    public class LocalBinder extends Binder {
+        public LocationSharingService getService() {
+            return LocationSharingService.this;
+        }
+    }
+}
diff --git a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
index e8bf9d814c2c50fed16285df5eafe94cf42738c9..f8b15f736a07af1d748d05f5c31469d2edad84a9 100644
--- a/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
+++ b/ring-android/app/src/main/java/cx/ring/services/NotificationServiceImpl.java
@@ -54,6 +54,7 @@ import java.io.File;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.TreeMap;
@@ -327,6 +328,32 @@ public class NotificationServiceImpl implements NotificationService {
         return messageNotificationBuilder.build();
     }
 
+    @Override
+    public void showLocationNotification(Account first, CallContact contact) {
+        android.net.Uri path = ConversationPath.toUri(first.getAccountID(), contact.getPrimaryUri());
+
+        Intent intentConversation = new Intent(Intent.ACTION_VIEW, path, mContext, ConversationActivity.class)
+                .putExtra(ConversationFragment.EXTRA_SHOW_MAP, true);
+
+        NotificationCompat.Builder messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_MESSAGE)
+                .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+                .setPriority(NotificationCompat.PRIORITY_HIGH)
+                .setDefaults(NotificationCompat.DEFAULT_ALL)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setSmallIcon(R.drawable.ic_ring_logo_white)
+                .setLargeIcon(getContactPicture(contact))
+                .setContentText(mContext.getString(R.string.location_share_contact, contact.getDisplayName()))
+                .setContentIntent(PendingIntent.getActivity(mContext, random.nextInt(), intentConversation, 0))
+                .setAutoCancel(false)
+                .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
+        notificationManager.notify(Objects.hash( "Location", path), messageNotificationBuilder.build());
+    }
+
+    @Override
+    public void cancelLocationNotification(Account first, CallContact contact) {
+        notificationManager.cancel(Objects.hash( "Location", ConversationPath.toUri(first.getAccountID(), contact.getPrimaryUri())));
+    }
+
     /**
      * Updates a notification
      *
diff --git a/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java b/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java
index e409e381687a1578b093b2644ed8094a28933f07..743f43cfc9f0f686dc24a13610ab2042c67b810b 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/ConversationPath.java
@@ -4,7 +4,13 @@ import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.jetbrains.annotations.Contract;
+
 import java.util.List;
+import java.util.Objects;
 
 import cx.ring.fragments.ConversationFragment;
 import cx.ring.model.Interaction;
@@ -16,6 +22,12 @@ public class ConversationPath {
         accountId = account;
         contactId = contact;
     }
+
+    public ConversationPath(@NonNull Tuple<String, String> path) {
+        accountId = path.first;
+        contactId = path.second;
+    }
+
     public String getAccountId() {
         return accountId;
     }
@@ -31,13 +43,13 @@ public class ConversationPath {
         builder = builder.appendEncodedPath(contactId);
         return builder.build();
     }
-    public static Uri toUri(String accountId, cx.ring.model.Uri contactUri) {
+    public static Uri toUri(String accountId, @NonNull cx.ring.model.Uri contactUri) {
         Uri.Builder builder = ContentUriHandler.CONVERSATION_CONTENT_URI.buildUpon();
         builder = builder.appendEncodedPath(accountId);
         builder = builder.appendEncodedPath(contactUri.getUri());
         return builder.build();
     }
-    public static Uri toUri(Interaction interaction) {
+    public static Uri toUri(@NonNull Interaction interaction) {
         return toUri(interaction.getAccount(), new cx.ring.model.Uri(interaction.getConversation().getParticipant()));
     }
 
@@ -64,7 +76,9 @@ public class ConversationPath {
         }
         return null;
     }
-    public static ConversationPath fromBundle(Bundle bundle) {
+
+    @Contract("null -> null")
+    public static ConversationPath fromBundle(@Nullable Bundle bundle) {
         if (bundle != null) {
             String accountId = bundle.getString(ConversationFragment.KEY_ACCOUNT_ID);
             String contactId = bundle.getString(ConversationFragment.KEY_CONTACT_RING_ID);
@@ -75,7 +89,8 @@ public class ConversationPath {
         return null;
     }
 
-    public static ConversationPath fromIntent(Intent intent) {
+    @Contract("null -> null")
+    public static ConversationPath fromIntent(@Nullable Intent intent) {
         if (intent != null) {
             Uri uri = intent.getData();
             ConversationPath conversationPath = fromUri(uri);
@@ -85,4 +100,20 @@ public class ConversationPath {
         }
         return null;
     }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == this)
+            return true;
+        if (obj == null || obj.getClass() != getClass())
+            return false;
+        ConversationPath o = (ConversationPath) obj;
+        return Objects.equals(o.accountId, accountId)
+                && Objects.equals(o.contactId, contactId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(accountId, contactId);
+    }
 }
diff --git a/ring-android/app/src/main/java/cx/ring/utils/TouchClickListener.java b/ring-android/app/src/main/java/cx/ring/utils/TouchClickListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5c079b80331a6da9d7b917d9b3cc66189aa2e7f
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/utils/TouchClickListener.java
@@ -0,0 +1,54 @@
+package cx.ring.utils;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class TouchClickListener implements GestureDetector.OnGestureListener, View.OnTouchListener {
+    private final View.OnClickListener onClickListener;
+    private final GestureDetector mGestureDetector;
+    private View view;
+
+    public TouchClickListener(Context c, View.OnClickListener l) {
+        onClickListener = l;
+        mGestureDetector = new GestureDetector(c, this);
+    }
+
+    @Override
+    public boolean onDown(MotionEvent e) {
+        return false;
+    }
+
+    @Override
+    public void onShowPress(MotionEvent e) {}
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        onClickListener.onClick(view);
+        return true;
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        return false;
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {}
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        return false;
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        view = v;
+        mGestureDetector.onTouchEvent(event);
+        view = null;
+        return true;
+    }
+}
diff --git a/ring-android/app/src/main/res/drawable/baseline_close_24.xml b/ring-android/app/src/main/res/drawable/baseline_close_24.xml
new file mode 100755
index 0000000000000000000000000000000000000000..16d6d37dd9f765d258b2876d79f88c162b007980
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/baseline_close_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/ring-android/app/src/main/res/drawable/baseline_location_disabled_24.xml b/ring-android/app/src/main/res/drawable/baseline_location_disabled_24.xml
new file mode 100755
index 0000000000000000000000000000000000000000..28a6822c6ff143f14995fe6260a1d776fc116472
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/baseline_location_disabled_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06c-1.13,0.12 -2.19,0.46 -3.16,0.97l1.5,1.5C10.16,5.19 11.06,5 12,5c3.87,0 7,3.13 7,7 0,0.94 -0.19,1.84 -0.52,2.65l1.5,1.5c0.5,-0.96 0.84,-2.02 0.97,-3.15L23,13v-2h-2.06zM3,4.27l2.04,2.04C3.97,7.62 3.25,9.23 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c1.77,-0.2 3.38,-0.91 4.69,-1.98L19.73,21 21,19.73 4.27,3 3,4.27zM16.27,17.54C15.09,18.45 13.61,19 12,19c-3.87,0 -7,-3.13 -7,-7 0,-1.61 0.55,-3.09 1.46,-4.27l9.81,9.81z"/>
+</vector>
diff --git a/ring-android/app/src/main/res/drawable/baseline_more_vert_24.xml b/ring-android/app/src/main/res/drawable/baseline_more_vert_24.xml
new file mode 100755
index 0000000000000000000000000000000000000000..34b93ecdf2a2ca9e315189c17a790e5ba3eda59b
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/baseline_more_vert_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
+</vector>
diff --git a/ring-android/app/src/main/res/drawable/baseline_my_location_24.xml b/ring-android/app/src/main/res/drawable/baseline_my_location_24.xml
new file mode 100755
index 0000000000000000000000000000000000000000..64266bd3923bba2207826a20e417d5977ef6e0f3
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/baseline_my_location_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
+</vector>
diff --git a/ring-android/app/src/main/res/drawable/baseline_navigation_24.xml b/ring-android/app/src/main/res/drawable/baseline_navigation_24.xml
new file mode 100755
index 0000000000000000000000000000000000000000..421447cb3b768eaa6afc2be66c16281793be4535
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/baseline_navigation_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2L4.5,20.29l0.71,0.71L12,18l6.79,3 0.71,-0.71z"/>
+</vector>
diff --git a/ring-android/app/src/main/res/drawable/loccationshare_bg_gradient.xml b/ring-android/app/src/main/res/drawable/loccationshare_bg_gradient.xml
new file mode 100644
index 0000000000000000000000000000000000000000..41724ebf94aaac7141aec457b64c6994f6f3b124
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable/loccationshare_bg_gradient.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <gradient
+                android:angle="270"
+                android:startColor="@color/colorPrimaryTranslucent"
+                android:endColor="#00000000"
+                android:type="linear" />
+        </shape>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/drawable/msg_input_bg.xml b/ring-android/app/src/main/res/drawable/textmsg_bg_input.xml
similarity index 100%
rename from ring-android/app/src/main/res/drawable/msg_input_bg.xml
rename to ring-android/app/src/main/res/drawable/textmsg_bg_input.xml
diff --git a/ring-android/app/src/main/res/layout/frag_conversation.xml b/ring-android/app/src/main/res/layout/frag_conversation.xml
index e467652f502fdb5fa27535b625f37962b8fe127d..bb93f5cdd1fbbd0f0683d31e6e24feb8cad2b21b 100644
--- a/ring-android/app/src/main/res/layout/frag_conversation.xml
+++ b/ring-android/app/src/main/res/layout/frag_conversation.xml
@@ -14,7 +14,8 @@
             android:layout_height="match_parent"
             android:background="@color/background"
             android:clipToPadding="false"
-            android:paddingTop="?attr/actionBarSize">
+            android:paddingTop="?attr/actionBarSize"
+            android:animateLayoutChanges="true">
 
             <LinearLayout
                 android:id="@+id/ongoingcall_pane"
@@ -62,6 +63,32 @@
 
             </LinearLayout>
 
+            <androidx.cardview.widget.CardView
+                android:id="@+id/mapCard"
+                android:layout_width="@dimen/location_sharing_minmap_width"
+                android:layout_height="@dimen/location_sharing_minmap_height"
+                android:layout_below="@id/trustRequestMessageLayout"
+                android:layout_centerHorizontal="true"
+                android:layout_marginHorizontal="0dp"
+                android:layout_marginVertical="16dp"
+                android:animateLayoutChanges="true"
+                android:clickable="true"
+                android:descendantFocusability="blocksDescendants"
+                android:focusable="true"
+                android:visibility="gone"
+                app:cardCornerRadius="16dp"
+                app:cardElevation="4dp"
+                tools:visibility="visible">
+
+                <FrameLayout
+                    android:id="@+id/mapLayout"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    tools:background="@color/light_green_400"
+                    android:clickable="false" />
+
+            </androidx.cardview.widget.CardView>
+
             <ProgressBar
                 android:id="@+id/pb_loading"
                 android:layout_width="64dp"
@@ -83,6 +110,7 @@
                 android:paddingTop="8dp"
                 android:paddingBottom="60dp"
                 android:transcriptMode="normal"
+                android:animateLayoutChanges="false"
                 app:layoutManager="LinearLayoutManager"
                 app:stackFromEnd="true"
                 tools:listitem="@layout/item_conv_msg_peer" />
@@ -103,7 +131,7 @@
                 android:layout_alignParentBottom="true"
                 android:layout_marginLeft="8dp"
                 android:layout_marginRight="8dp"
-                android:layout_marginBottom="12dp"
+                android:layout_marginBottom="8dp"
                 android:padding="0dp"
                 android:visibility="gone"
                 app:cardBackgroundColor="#4CAF50"
@@ -131,7 +159,7 @@
                 android:layout_alignParentBottom="true"
                 android:layout_marginLeft="8dp"
                 android:layout_marginRight="8dp"
-                android:layout_marginBottom="12dp"
+                android:layout_marginBottom="8dp"
                 android:padding="0dp"
                 android:visibility="gone"
                 app:cardCornerRadius="@dimen/conversation_message_input_radius">
@@ -187,7 +215,7 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_alignParentBottom="true"
-                android:background="@drawable/msg_input_bg"
+                android:background="@drawable/textmsg_bg_input"
                 android:orientation="vertical"
                 android:visibility="visible"
                 tools:visibility="visible">
@@ -196,10 +224,10 @@
                     android:id="@+id/cvMessageInput"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
+                    android:layout_marginLeft="12dp"
                     android:layout_marginTop="2dp"
                     android:layout_marginRight="12dp"
-                    android:layout_marginBottom="12dp"
-                    android:layout_marginLeft="12dp"
+                    android:layout_marginBottom="8dp"
                     android:visibility="gone"
                     app:cardCornerRadius="@dimen/conversation_message_input_radius"
                     app:cardElevation="4dp"
@@ -220,20 +248,19 @@
                             android:contentDescription="@string/share_label"
                             android:onClick="@{v -> presenter.expandMenu(v)}"
                             android:padding="8dp"
-                            android:tint="@android:color/darker_gray"
-                            app:srcCompat="@drawable/baseline_expand_less_24" />
+                            android:tint="@color/colorPrimary"
+                            app:srcCompat="@drawable/baseline_more_vert_24" />
 
                         <ImageButton
                             android:id="@+id/btn_take_picture"
-                            android:layout_width="wrap_content"
+                            android:layout_width="34dp"
                             android:layout_height="match_parent"
-                            android:layout_marginEnd="5dp"
                             android:background="?selectableItemBackgroundBorderless"
                             android:contentDescription="@string/take_a_photo"
                             android:onClick="@{() -> presenter.takePicture()}"
                             android:padding="8dp"
-                            android:tint="@android:color/darker_gray"
-                            app:srcCompat="@drawable/baseline_photo_camera_24" />
+                            android:tint="@color/colorPrimary"
+                            app:srcCompat="@drawable/baseline_photo_camera_24"/>
 
                         <ProgressBar
                             android:id="@+id/pb_data_transfer"
@@ -293,6 +320,7 @@
     </FrameLayout>
 
     <data>
+
         <variable
             name="presenter"
             type="cx.ring.fragments.ConversationFragment" />
diff --git a/ring-android/app/src/main/res/layout/frag_location_sharing.xml b/ring-android/app/src/main/res/layout/frag_location_sharing.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d9831f7fee8292be29da4fc037670c23ce920508
--- /dev/null
+++ b/ring-android/app/src/main/res/layout/frag_location_sharing.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:animateLayoutChanges="true"
+    tools:context=".fragments.LocationSharingFragment">
+
+    <org.osmdroid.views.MapView
+        android:id="@+id/map"
+        tilesource="Mapnik"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:animateLayoutChanges="false"
+        tools:background="@color/green_400" />
+
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/locshare_toolbar"
+        style="@style/Widget.MaterialComponents.Toolbar.Surface"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:elevation="4dp"
+        app:navigationIcon="@drawable/baseline_close_24"
+        app:title="Location Sharing"
+        android:visibility="gone"
+        tools:subtitle="Jean is sharing his location with you"
+        tools:visibility="gone"/>
+
+    <LinearLayout
+        android:id="@+id/locshare_snipet"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/locshare_snipet_txt"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end"
+            android:background="@color/colorPrimaryTranslucent"
+            android:paddingTop="4dp"
+            android:paddingHorizontal="16dp"
+            tools:text="Jean is sharing his location with you"
+            android:textColor="@color/colorOnPrimary"
+            android:gravity="end"/>
+
+        <ImageView
+            android:id="@+id/locshare_snipet_txt_shadow"
+            android:layout_width="match_parent"
+            android:layout_height="8dp"
+            android:background="@drawable/loccationshare_bg_gradient"
+            tools:ignore="ContentDescription" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/shareControls"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:animateLayoutChanges="true"
+        android:orientation="vertical"
+        android:visibility="gone"
+        tools:visibility="visible">
+
+        <com.google.android.material.floatingactionbutton.FloatingActionButton
+            android:id="@+id/btn_center_position"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end"
+            android:layout_margin="16dp"
+            android:src="@drawable/baseline_my_location_24"
+            app:backgroundTint="@color/background"
+            app:elevation="2dp"
+            app:fabSize="mini"
+            app:rippleColor="@color/grey_400"
+            app:tint="@color/colorPrimary" />
+
+        <com.google.android.material.chip.ChipGroup
+            android:id="@+id/location_share_time_group"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            app:checkedChip="@id/location_share_time_1h"
+            app:singleLine="true"
+            app:singleSelection="true">
+
+            <com.google.android.material.chip.Chip
+                android:id="@+id/location_share_time_1h"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:checkable="true"
+                android:checked="true"
+                android:text="1 hour" />
+
+            <com.google.android.material.chip.Chip
+                android:id="@+id/location_share_time_10m"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:checkable="true"
+                android:text="10 minutes" />
+
+        </com.google.android.material.chip.ChipGroup>
+
+        <com.google.android.material.chip.Chip
+            android:id="@+id/location_share_time_remaining"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:checkable="false"
+            android:enabled="false"
+            android:focusable="false"
+            android:visibility="gone"
+            tools:text="43 minutes" />
+
+        <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+            android:id="@+id/btn_share_location"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginBottom="@dimen/action_button_lpadding"
+            android:text="@string/location_share_action_start"
+            app:icon="@drawable/baseline_navigation_24" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/shareControlsMini"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:orientation="vertical"
+        android:visibility="gone">
+
+        <com.google.android.material.chip.Chip
+            android:id="@+id/location_share_stop"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:text="@string/location_share_action_stop"
+            android:textColor="@color/white"
+            android:visibility="gone"
+            app:chipBackgroundColor="@color/design_default_color_error"
+            tools:visibility="visible" />
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/menu/conversation_share_actions.xml b/ring-android/app/src/main/res/menu/conversation_share_actions.xml
index 5a318adeb74a1ba76999a1d4e34d133f4d610b20..7a453cf81a4cafdc6ea8325556a88ae6cde3b82f 100644
--- a/ring-android/app/src/main/res/menu/conversation_share_actions.xml
+++ b/ring-android/app/src/main/res/menu/conversation_share_actions.xml
@@ -2,6 +2,14 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools">
+
+    <item
+        android:id="@+id/conv_share_location"
+        android:icon="@drawable/baseline_navigation_24"
+        android:title="@string/conversation_share_location"
+        app:showAsAction="always"
+        tools:ignore="AlwaysShowAction" />
+
     <item
         android:id="@+id/conv_send_audio"
         android:icon="@drawable/baseline_mic_24"
diff --git a/ring-android/app/src/main/res/values-night/colors.xml b/ring-android/app/src/main/res/values-night/colors.xml
index 62af0603fc0269c487e8d2d28afa0ac209a9caa5..a7e82e9b14aee1ba7e9d070a2144e0420a8b985a 100644
--- a/ring-android/app/src/main/res/values-night/colors.xml
+++ b/ring-android/app/src/main/res/values-night/colors.xml
@@ -6,6 +6,7 @@
     <color name="app_bar">@color/grey_950</color>
 
     <color name="colorPrimary">@color/white</color>
+    <color name="colorPrimaryTranslucent">#C0FFFFFF</color>
     <color name="colorOnPrimary">@color/color_primary_dark</color>
 
     <color name="colorSecondary">@color/color_primary_light</color>
diff --git a/ring-android/app/src/main/res/values/app_colors.xml b/ring-android/app/src/main/res/values/app_colors.xml
index d1b7b5491ff715ff9b177f1dfac43c913695a2cf..df21298618b93c0afd6e62f181d272cca77d86da 100644
--- a/ring-android/app/src/main/res/values/app_colors.xml
+++ b/ring-android/app/src/main/res/values/app_colors.xml
@@ -2,4 +2,7 @@
 <resources>
     <color name="color_primary_light">#28B1ED</color>
     <color name="color_primary_dark">#000747</color>
+
+    <color name="color_primary_light_translucent">#B028B1ED</color>
+    <color name="color_primary_dark_translucent">#B0000747</color>
 </resources>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/values/colors.xml b/ring-android/app/src/main/res/values/colors.xml
index 04e3e6a6a50e4811975bb728f180f6aed7a50010..f67ff89137f0f5890e093adb5674b1dcc8b79b74 100644
--- a/ring-android/app/src/main/res/values/colors.xml
+++ b/ring-android/app/src/main/res/values/colors.xml
@@ -6,6 +6,7 @@
     <color name="app_bar">@color/grey_25</color>
 
     <color name="colorPrimary">@color/color_primary_dark</color>
+    <color name="colorPrimaryTranslucent">@color/color_primary_dark_translucent</color>
     <color name="colorOnPrimary">@color/white</color>
 
     <color name="colorSecondary">@color/color_primary_light</color>
diff --git a/ring-android/app/src/main/res/values/dimens.xml b/ring-android/app/src/main/res/values/dimens.xml
index 4b374f2cef3621e3568ee4edcdb4d29aa6614992..02ad1337c44739d42d47ff52ba33dba7a17c8b29 100644
--- a/ring-android/app/src/main/res/values/dimens.xml
+++ b/ring-android/app/src/main/res/values/dimens.xml
@@ -33,6 +33,9 @@ along with this program; if not, write to the Free Software
     <dimen name="conversation_message_radius">18dp</dimen>
     <dimen name="conversation_message_minor_radius">3dp</dimen>
     <dimen name="conversation_timestamp_textsize">12sp</dimen>
+    <dimen name="location_sharing_avatar_size">36dp</dimen>
+    <dimen name="location_sharing_minmap_width">260dp</dimen>
+    <dimen name="location_sharing_minmap_height">160dp</dimen>
 
     <dimen name="activity_horizontal_margin">32dp</dimen>
     <dimen name="activity_vertical_margin">16dp</dimen>
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 35616e8287ef89eb629d65de8ba73f471c1faf6c..b737cf6c1460acd82206decfbe2f6b524e76b922 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -132,6 +132,13 @@ along with this program; if not, write to the Free Software
     <string name="notif_incoming_picture">Picture from %1$s</string>
     <string name="action_open">Open</string>
     <string name="notif_sync_title">Syncing data…</string>
+    <string name="notif_location_title">Sharing location with %1$s</string>
+    <string name="location_share_contact">%1$s is sharing location with you</string>
+    <string name="notif_location_multi_title">Sharing location with %1$d contacts</string>
+    <string name="notif_location_remaining">%1$d minutes remaining</string>
+    <string name="notif_location_action_stop">"Stop sharing location"</string>
+    <string name="location_share_action_start">"Start sharing"</string>
+    <string name="location_share_action_stop">"Stop sharing"</string>
 
     <!-- Call Fragment -->
     <string name="you_txt_prefix">You:</string>
@@ -167,6 +174,7 @@ along with this program; if not, write to the Free Software
     <string name="hist_file_received">File received</string>
     <string name="conversation_send_audio">Record audio clip</string>
     <string name="conversation_send_video">Record video clip</string>
+    <string name="conversation_share_location">Share location</string>
 
     <!-- MediaPreferenceFragment -->
     <string name="permission_dialog_camera_title">Camera permission</string>
diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
index 7611dfb72ca08cdde74d005ac72a181fb5962fd2..e091648f0cf951d7ef5aa551d145e485c376d6b7 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationPresenter.java
@@ -45,6 +45,7 @@ import cx.ring.services.HardwareService;
 import cx.ring.services.VCardService;
 import cx.ring.utils.Log;
 import cx.ring.utils.StringUtils;
+import cx.ring.utils.Tuple;
 import cx.ring.utils.VCardUtils;
 import io.reactivex.Scheduler;
 import io.reactivex.disposables.CompositeDisposable;
@@ -212,6 +213,15 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         mConversationDisposable.add(c.getColor()
                 .observeOn(mUiScheduler)
                 .subscribe(view::setConversationColor, e -> Log.e(TAG, "Can't update conversation color", e)));
+
+        Log.e(TAG, "getLocationUpdates subscribe");
+        mConversationDisposable.add(account
+                .getLocationUpdates(c.getContact().getPrimaryUri())
+                .observeOn(mUiScheduler)
+                .subscribe(u -> {
+                    Log.e(TAG, "getLocationUpdates: update");
+                    getView().showMap(c.getAccountId(), c.getContact().getPrimaryUri().getUri(), false);
+                }));
     }
 
     public void openContact() {
@@ -377,4 +387,11 @@ public class ConversationPresenter extends RootPresenter<ConversationView> {
         }
     }
 
+    public void shareLocation() {
+        getView().startShareLocation(mAccountId, mContactRingId.getUri());
+    }
+
+    public Tuple<String, String> getPath() {
+        return new Tuple<>(mAccountId, mContactRingId.getUri());
+    }
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
index 7cc5504959d5d3d3a0bba8c3714851d5b95b844c..b7b357b1ef4c0c80db9a4ea3cdb3c7fbec0f1438 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/conversation/ConversationView.java
@@ -79,4 +79,9 @@ public interface ConversationView extends BaseView {
     void setConversationColor(int integer);
 
     void startSaveFile(DataTransfer currentFile, String fileAbsolutePath);
+
+    void startShareLocation(String accountId, String contactId);
+
+    void showMap(String accountId, String contactId, boolean open);
+    void hideMap();
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
index bac767904a94b1abee6abd18e4a7fb4502d9c10e..80115e7e72a4e693c0d0b01b2333021896b0d880 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/facades/ConversationFacade.java
@@ -24,6 +24,7 @@ import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.NavigableMap;
+import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 
@@ -49,6 +50,7 @@ import cx.ring.services.PreferencesService;
 import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.FileUtils;
 import cx.ring.utils.Log;
+import cx.ring.utils.Tuple;
 import io.reactivex.Completable;
 import io.reactivex.Observable;
 import io.reactivex.Single;
@@ -127,6 +129,31 @@ public class ConversationFacade {
                 .subscribe(this::parseNewMessage,
                         e -> Log.e(TAG, "Error adding text message", e)));
 
+        mDisposableBag.add(mAccountService.getLocationUpdates()
+                .concatMapSingle(location -> getAccountSubject(location.getAccount())
+                        .map(a -> {
+                            long expiration = a.onLocationUpdate(location);
+                            mDisposableBag.add(Completable.timer(expiration, TimeUnit.MILLISECONDS)
+                                    .subscribe(a::maintainLocation));
+                            return location;
+                        }))
+                .subscribe());
+
+        mDisposableBag.add(mAccountService.getObservableAccountList()
+                .switchMap(accounts -> {
+                    List<Observable<Tuple<Account, Account.ContactLocationEntry>>> r = new ArrayList<>(accounts.size());
+                    for (Account a : accounts)
+                        r.add(a.getLocationUpdates().map(s -> new Tuple<>(a, s)));
+                    return Observable.merge(r);
+                })
+                .distinctUntilChanged()
+                .subscribe(t -> {
+                    Log.e(TAG, "Location reception started for " + t.second.contact);
+                    mNotificationService.showLocationNotification(t.first, t.second.contact);
+                    mDisposableBag.add(t.second.location.doOnComplete(() ->
+                            mNotificationService.cancelLocationNotification(t.first, t.second.contact)).subscribe());
+                }));
+
         mDisposableBag.add(mAccountService
                 .getMessageStateChanges()
                 .concatMapSingle(txt -> getAccountSubject(txt.getAccount())
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/Account.java b/ring-android/libringclient/src/main/java/cx/ring/model/Account.java
index 987121337b0433b5a5bdee7a465064f48312d06d..8757ffea3145ad59babf1ea6406dc505f37d0c26 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/Account.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/Account.java
@@ -26,16 +26,18 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
 import cx.ring.model.Interaction.InteractionStatus;
+import cx.ring.services.AccountService;
 import cx.ring.smartlist.SmartListViewModel;
 import cx.ring.utils.Log;
 import cx.ring.utils.StringUtils;
 import cx.ring.utils.Tuple;
 import ezvcard.VCard;
-import ezvcard.property.FormattedName;
+import io.reactivex.Maybe;
 import io.reactivex.Observable;
 import io.reactivex.Single;
 import io.reactivex.subjects.BehaviorSubject;
@@ -49,6 +51,7 @@ public class Account {
     private static final String CONTACT_CONFIRMED = "confirmed";
     private static final String CONTACT_BANNED = "banned";
     private static final String CONTACT_ID = "id";
+    private static final int LOCATION_SHARING_EXPIRATION_MS = 1000 * 60 * 2;
 
     private final String accountID;
 
@@ -83,6 +86,20 @@ public class Account {
     private final BehaviorSubject<Collection<CallContact>> contactListSubject = BehaviorSubject.create();
     private final BehaviorSubject<Collection<TrustRequest>> trustRequestsSubject = BehaviorSubject.create();
 
+    public static class ContactLocation {
+        public double latitude;
+        public double longitude;
+        public long timestamp;
+        public Date receivedDate;
+    }
+    public static class ContactLocationEntry {
+        public CallContact contact;
+        public Observable<ContactLocation> location;
+    }
+    private final Map<CallContact, Observable<ContactLocation>> contactLocations = new HashMap<>();
+    private final Subject<Map<CallContact, Observable<ContactLocation>>> mLocationSubject = BehaviorSubject.createDefault(contactLocations);
+    private final Subject<ContactLocationEntry> mLocationStartedSubject = PublishSubject.create();
+
     public Single<Account> historyLoader;
     private VCard mProfile;
     private Single<Tuple<String, Object>> mLoadedProfile = null;
@@ -856,6 +873,74 @@ public class Account {
         }
     }
 
+    synchronized public long onLocationUpdate(AccountService.Location location) {
+        Log.w(TAG, "onLocationUpdate " + location.getPeer() + " " + location.getLatitude() + ",  " + location.getLongitude());
+        CallContact contact = getContactFromCache(location.getPeer());
+
+        ContactLocation cl = new ContactLocation();
+        cl.timestamp = location.getDate();
+        cl.latitude = location.getLatitude();
+        cl.longitude = location.getLongitude();
+        cl.receivedDate = new Date();
+
+        Observable<ContactLocation> ls = contactLocations.get(contact);
+        if (ls == null) {
+            ls = BehaviorSubject.createDefault(cl);
+            contactLocations.put(contact, ls);
+            mLocationSubject.onNext(contactLocations);
+            ContactLocationEntry entry = new ContactLocationEntry();
+            entry.contact = contact;
+            entry.location = ls;
+            mLocationStartedSubject.onNext(entry);
+        } else {
+            if (ls.blockingFirst().timestamp < cl.timestamp)
+                ((Subject<ContactLocation>) ls).onNext(cl);
+        }
+        return cl.receivedDate.getTime() + LOCATION_SHARING_EXPIRATION_MS;
+    }
+
+    synchronized public void maintainLocation() {
+        Log.w(TAG, "maintainLocation " + contactLocations.size());
+        if (contactLocations.isEmpty())
+            return;
+        boolean changed = false;
+
+        final Date expiration = new Date(System.currentTimeMillis() - LOCATION_SHARING_EXPIRATION_MS);
+        Iterator<Map.Entry<CallContact, Observable<ContactLocation>>> it = contactLocations.entrySet().iterator();
+        while (it.hasNext())  {
+            Map.Entry<CallContact, Observable<ContactLocation>> e = it.next();
+            if (e.getValue().blockingFirst().receivedDate.before(expiration)) {
+                Log.w(TAG, "maintainLocation clearing " + e.getKey().getDisplayName());
+                ((Subject<ContactLocation>) e.getValue()).onComplete();
+                changed = true;
+                it.remove();
+            }
+        }
+
+        if (changed)
+            mLocationSubject.onNext(contactLocations);
+    }
+
+    public Observable<ContactLocationEntry> getLocationUpdates() {
+        return mLocationStartedSubject;
+    }
+
+    public Observable<Map<CallContact, Observable<ContactLocation>>> getLocationsUpdates() {
+        return mLocationSubject;
+    }
+
+    public Observable<Observable<ContactLocation>> getLocationUpdates(Uri contactId) {
+        CallContact contact = getContactFromCache(contactId);
+        Log.w(TAG, "getLocationUpdates " + contactId + " " + contact);
+        return mLocationSubject
+                .flatMapMaybe(locations -> {
+                    Observable<ContactLocation> r = locations.get(contact);
+                    Log.w(TAG, "getLocationUpdates flatMapMaybe " + locations.size() + " " + r);
+                    return r == null ? Maybe.empty() : Maybe.just(r);
+                })
+                .distinctUntilChanged();
+    }
+
     public Single<String> getAccountAlias() {
         if (mLoadedProfile == null)
             return Single.just(getAlias());
diff --git a/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java b/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java
index 2fcbc9f46a142cb4440ae49501081ee45bf7c9ef..df8face377c89551656f4d84259d3d792e74f820 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/model/SipCall.java
@@ -227,7 +227,7 @@ public class SipCall extends Interaction {
         for (Map.Entry<String, String> message : messages.entrySet()) {
             HashMap<String, String> messageKeyValue = VCardUtils.parseMimeAttributes(message.getKey());
             String mimeType = messageKeyValue.get(VCardUtils.VCARD_KEY_MIME_TYPE);
-            if (!VCardUtils.MIME_RING_PROFILE_VCARD.equals(mimeType)) {
+            if (!VCardUtils.MIME_PROFILE_VCARD.equals(mimeType)) {
                 continue;
             }
             int part = Integer.parseInt(messageKeyValue.get(VCardUtils.VCARD_KEY_PART));
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java b/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java
index c0f3a4ea97ee4d2c23d8ed06b307f35360579b37..71e8d6017cffc751c5b703fcc757e65e828cd5b0 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/AccountService.java
@@ -20,6 +20,9 @@
  */
 package cx.ring.services;
 
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
 import java.io.File;
 import java.net.SocketException;
 import java.util.ArrayList;
@@ -30,14 +33,12 @@ import java.util.Random;
 import java.util.Timer;
 import java.util.TimerTask;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 
 import cx.ring.daemon.Blob;
 import cx.ring.daemon.DataTransferInfo;
-import cx.ring.daemon.Message;
 import cx.ring.daemon.Ringservice;
 import cx.ring.daemon.StringMap;
 import cx.ring.daemon.StringVect;
@@ -63,6 +64,7 @@ import cx.ring.utils.SwigNativeConverter;
 import cx.ring.utils.VCardUtils;
 import ezvcard.VCard;
 import io.reactivex.Completable;
+import io.reactivex.Maybe;
 import io.reactivex.Observable;
 import io.reactivex.Single;
 import io.reactivex.schedulers.Schedulers;
@@ -104,7 +106,6 @@ public class AccountService {
     private List<Account> mAccountList = new ArrayList<>();
     private boolean mHasSipAccount;
     private boolean mHasRingAccount;
-    private AtomicBoolean mAccountsLoaded = new AtomicBoolean(false);
 
     private final HashMap<Long, DataTransfer> mDataTransfers = new HashMap<>();
     private DataTransfer mStartingTransfer = null;
@@ -117,9 +118,81 @@ public class AccountService {
             .map(l -> l.get(0))
             .distinctUntilChanged();
 
-    private final Subject<TextMessage> incomingMessageSubject = PublishSubject.create();
-    private final Subject<TextMessage> messageSubject = PublishSubject.create();
+    public static class Message {
+        String accountId;
+        String callId;
+        String author;
+        Map<String, String> messages;
+    }
+    public static class Location {
+        String accountId;
+        String callId;
+        Uri peer;
+        long time;
+        double latitude;
+        double longitude;
+
+        public String getAccount() {
+            return accountId;
+        }
+
+        public Uri getPeer() {
+            return peer;
+        }
+
+        public long getDate() {
+            return time;
+        }
+
+        public double getLatitude() {
+            return latitude;
+        }
+
+        public double getLongitude() {
+            return longitude;
+        }
+    }
+
+    private final Subject<Message> incomingMessageSubject = PublishSubject.create();
+
+    private final Observable<TextMessage> incomingTextMessageSubject = incomingMessageSubject
+            .flatMapMaybe(msg -> {
+                String message = msg.messages.get(CallService.MIME_TEXT_PLAIN);
+                if (message != null) {
+                    return mHistoryService
+                            .incomingMessage(msg.accountId, msg.callId, msg.author, message)
+                            .toMaybe();
+                }
+                return Maybe.empty();
+            })
+            .share();
+
+    private final Observable<Location> incomingLocationSubject = incomingMessageSubject
+            .flatMapMaybe(msg -> {
+                try {
+                    String loc = msg.messages.get(CallService.MIME_GEOLOCATION);
+                    if (loc == null)
+                        return Maybe.empty();
+
+                    JsonObject obj = JsonParser.parseString(loc).getAsJsonObject();
+                    if (obj.size() < 2)
+                        return Maybe.empty();
+                    Location l = new Location();
+                    l.latitude = obj.get("lat").getAsDouble();
+                    l.longitude = obj.get("long").getAsDouble();
+                    l.time = obj.get("time").getAsLong();
+                    l.accountId = msg.accountId;
+                    l.callId = msg.callId;
+                    l.peer = new Uri(msg.author);
+                    return Maybe.just(l);
+                } catch (Exception e) {
+                    Log.w(TAG, "Failed to receive geolocation", e);
+                    return Maybe.empty();
+                }
+            })
+            .share();
 
+    private final Subject<TextMessage> textMessageSubject = PublishSubject.create();
     private final Subject<DataTransfer> dataTransferSubject = PublishSubject.create();
     private final Subject<TrustRequest> incomingRequestsSubject = PublishSubject.create();
 
@@ -127,7 +200,7 @@ public class AccountService {
         accountsSubject.onNext(mAccountList);
     }
 
-    public class RegisteredName {
+    public static class RegisteredName {
         public String accountId;
         public String name;
         public String address;
@@ -136,19 +209,19 @@ public class AccountService {
 
     private final Subject<RegisteredName> registeredNameSubject = PublishSubject.create();
 
-    private class ExportOnRingResult {
+    private static class ExportOnRingResult {
         String accountId;
         int code;
         String pin;
     }
 
-    private class DeviceRevocationResult {
+    private static class DeviceRevocationResult {
         String accountId;
         String deviceId;
         int code;
     }
 
-    private class MigrationResult {
+    private static class MigrationResult {
         String accountId;
         String state;
     }
@@ -162,11 +235,15 @@ public class AccountService {
     }
 
     public Observable<TextMessage> getIncomingMessages() {
-        return incomingMessageSubject;
+        return incomingTextMessageSubject;
+    }
+
+    public Observable<Location> getLocationUpdates() {
+        return incomingLocationSubject;
     }
 
     public Observable<TextMessage> getMessageStateChanges() {
-        return messageSubject;
+        return textMessageSubject;
     }
 
     public Observable<TrustRequest> getIncomingRequests() {
@@ -187,10 +264,6 @@ public class AccountService {
         return mHasRingAccount;
     }
 
-    public boolean isLoaded() {
-        return mAccountsLoaded.get();
-    }
-
     /**
      * Loads the accounts from the daemon and then builds the local cache (also sends ACCOUNTS_CHANGED event)
      *
@@ -205,7 +278,6 @@ public class AccountService {
 
     private void refreshAccountsCacheFromDaemon() {
         Log.w(TAG, "refreshAccountsCacheFromDaemon");
-        mAccountsLoaded.set(false);
         boolean hasSip = false, hasJami = false;
         List<Account> curList = mAccountList;
         List<String> accountIds = new ArrayList<>(Ringservice.getAccountList());
@@ -300,7 +372,6 @@ public class AccountService {
         }, e -> Log.e(TAG, "Error completing profile migration", e));
 
         accountsSubject.onNext(newAccounts);
-        mAccountsLoaded.set(true);
     }
 
     private Account getAccountByName(final String name) {
@@ -499,7 +570,7 @@ public class AccountService {
                     while (i <= nbTotal) {
                         HashMap<String, String> chunk = new HashMap<>();
                         Log.d(TAG, "length vcard " + stringVCard.length() + " id " + key + " part " + i + " nbTotal " + nbTotal);
-                        String keyHashMap = VCardUtils.MIME_RING_PROFILE_VCARD + "; id=" + key + ",part=" + i + ",of=" + nbTotal;
+                        String keyHashMap = VCardUtils.MIME_PROFILE_VCARD + "; id=" + key + ",part=" + i + ",of=" + nbTotal;
                         String message = stringVCard.substring(0, Math.min(VCARD_CHUNK_SIZE, stringVCard.length()));
                         chunk.put(keyHashMap, message);
                         Ringservice.sendTextMessage(callId, StringMap.toSwig(chunk), "Me", false);
@@ -738,9 +809,7 @@ public class AccountService {
         mExecutor.execute(() -> {
             UintVect list = new UintVect();
             list.reserve(codecs.size());
-            for (Long codec : codecs) {
-                list.add(codec);
-            }
+            list.addAll(codecs);
             Ringservice.setActiveCodecList(accountId, list);
             accountSubject.onNext(getAccount(accountId));
         });
@@ -1167,19 +1236,19 @@ public class AccountService {
 
     void incomingAccountMessage(String accountId, String callId, String from, Map<String, String> messages) {
         Log.d(TAG, "incomingAccountMessage: " + accountId + " " + messages.size());
-        String message = messages.get(CallService.MIME_TEXT_PLAIN);
-        if (message != null) {
-            mHistoryService
-                    .incomingMessage(accountId, callId, from, message)
-                    .subscribe(incomingMessageSubject::onNext);
-        }
+        Message message = new Message();
+        message.accountId = accountId;
+        message.callId = callId;
+        message.author = from;
+        message.messages = messages;
+        incomingMessageSubject.onNext(message);
     }
 
     void accountMessageStatusChanged(String accountId, long messageId, String to, int status) {
         Log.d(TAG, "accountMessageStatusChanged: " + accountId + ", " + messageId + ", " + to + ", " + status);
         mHistoryService
                 .accountMessageStatusChanged(accountId, messageId, to, status)
-                .subscribe(messageSubject::onNext, e -> Log.e(TAG, "Error updating message", e));
+                .subscribe(textMessageSubject::onNext, e -> Log.e(TAG, "Error updating message: " + e.getLocalizedMessage()));
     }
 
     void errorAlert(int alert) {
@@ -1322,7 +1391,7 @@ public class AccountService {
         return DataTransferError.UNKNOWN;
     }
 
-    public List<Message> getLastMessages(String accountId, long baseTime) {
+    public List<cx.ring.daemon.Message> getLastMessages(String accountId, long baseTime) {
         try {
             return mExecutor.submit(() -> SwigNativeConverter.toJava(Ringservice.getLastMessages(accountId, baseTime))).get();
         } catch (Exception e) {
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java b/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java
index 8db3dea3c42f7053e35b7f74ba852cf786c91880..e673b0d6f5dc51eb577d04d7b8f5f0b4541b57c9 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/CallService.java
@@ -56,6 +56,7 @@ public class CallService {
 
     private final static String TAG = CallService.class.getSimpleName();
     public final static String MIME_TEXT_PLAIN = "text/plain";
+    public static final String MIME_GEOLOCATION = "application/geo";
 
     @Inject
     @Named("DaemonExecutor")
diff --git a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
index 4fcf8eb9e3c83cb398e82b0bd304cfa969a4e4de..c248abd4d95d5578089b6bca2f44d4eb5504ac1b 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/services/NotificationService.java
@@ -69,4 +69,8 @@ public interface NotificationService {
     Object getDataTransferNotification(int notificationId);
 
     void onConnectionUpdate(Boolean b);
+
+    void showLocationNotification(Account first, CallContact contact);
+    void cancelLocationNotification(Account first, CallContact contact);
+
 }
diff --git a/ring-android/libringclient/src/main/java/cx/ring/utils/VCardUtils.java b/ring-android/libringclient/src/main/java/cx/ring/utils/VCardUtils.java
index 304a66fc1c78195b3707883ba7ff289522faf5a4..4744306abd1e72c5a82a92b0313271bb4285092b 100644
--- a/ring-android/libringclient/src/main/java/cx/ring/utils/VCardUtils.java
+++ b/ring-android/libringclient/src/main/java/cx/ring/utils/VCardUtils.java
@@ -37,7 +37,7 @@ import io.reactivex.schedulers.Schedulers;
 public final class VCardUtils {
     public static final String TAG = VCardUtils.class.getSimpleName();
 
-    public static final String MIME_RING_PROFILE_VCARD = "x-ring/ring.profile.vcard";
+    public static final String MIME_PROFILE_VCARD = "x-ring/ring.profile.vcard";
     public static final String VCARD_KEY_MIME_TYPE = "mimeType";
     public static final String VCARD_KEY_PART = "part";
     public static final String VCARD_KEY_OF = "of";