Show and track the current location of the user on map.

This commit is contained in:
Onuray Sahin 2021-12-15 21:07:16 +03:00
parent 824e713c51
commit 5904a5955f
7 changed files with 235 additions and 4 deletions

View file

@ -488,6 +488,7 @@ dependencies {
// MapTiler
implementation 'org.maplibre.gl:android-sdk:9.5.2'
implementation 'org.maplibre.gl:android-plugin-annotation-v9:1.0.0'
// TESTS

View file

@ -61,6 +61,7 @@ import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.RoomDetailFragment
import im.vector.app.features.home.room.detail.search.SearchFragment
import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.location.LocationSharingFragment
import im.vector.app.features.login.LoginCaptchaFragment
import im.vector.app.features.login.LoginFragment
import im.vector.app.features.login.LoginGenericTextInputFormFragment
@ -855,4 +856,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(CreatePollFragment::class)
fun bindCreatePollFragment(fragment: CreatePollFragment): Fragment
@Binds
@IntoMap
@FragmentKey(LocationSharingFragment::class)
fun bindLocationSharingFragment(fragment: LocationSharingFragment): Fragment
}

View file

@ -16,22 +16,55 @@
package im.vector.app.features.location
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
import com.mapbox.mapboxsdk.style.layers.Property
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentLocationSharingBinding
import im.vector.app.features.home.AvatarRenderer
import org.billcarsonfr.jsonviewer.Utils
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class LocationSharingFragment @Inject constructor() :
VectorBaseFragment<FragmentLocationSharingBinding>() {
class LocationSharingFragment @Inject constructor(
private val locationTracker: LocationTracker,
private val session: Session,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTracker.Callback {
init {
locationTracker.callback = this
}
private val viewModel: LocationSharingViewModel by activityViewModel()
private val glideRequests by lazy {
GlideApp.with(this)
}
private var map: MapboxMap? = null
private var symbolManager: SymbolManager? = null
private var lastZoomValue: Double = -1.0
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationSharingBinding {
return FragmentLocationSharingBinding.inflate(inflater, container, false)
}
@ -48,12 +81,73 @@ class LocationSharingFragment @Inject constructor() :
initMapView(savedInstanceState)
}
override fun onDestroyView() {
super.onDestroyView()
locationTracker.stop()
}
private fun initMapView(savedInstanceState: Bundle?) {
val key = BuildConfig.mapTilerKey
val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=${key}"
val styleUrl = "https://api.maptiler.com/maps/streets/style.json?key=$key"
views.mapView.onCreate(savedInstanceState)
views.mapView.getMapAsync { map ->
map.setStyle(styleUrl)
map.setStyle(styleUrl) { style ->
addUserPinToMap(style)
this.symbolManager = SymbolManager(views.mapView, map, style)
this.map = map
// All set, start location tracker
locationTracker.start()
}
}
}
private fun addUserPinToMap(style: Style) {
session.getUser(session.myUserId)?.toMatrixItem()?.let {
val size = Utils.dpToPx(44, requireContext())
avatarRenderer.render(glideRequests, it, object : CustomTarget<Drawable>(size, size) {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
val bgUserPin = ContextCompat.getDrawable(requireActivity(), R.drawable.bg_map_user_pin)!!
val layerDrawable = LayerDrawable(arrayOf(bgUserPin, resource))
val horizontalInset = Utils.dpToPx(4, requireContext())
val topInset = Utils.dpToPx(4, requireContext())
val bottomInset = Utils.dpToPx(8, requireContext())
layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset)
style.addImage(
USER_PIN_NAME,
layerDrawable
)
}
override fun onLoadCleared(placeholder: Drawable?) {
// Is it possible? Put placeholder instead?
}
})
}
}
override fun onLocationUpdate(latitude: Double, longitude: Double) {
lastZoomValue = if (lastZoomValue == -1.0) INITIAL_ZOOM else map?.cameraPosition?.zoom ?: INITIAL_ZOOM
val latLng = LatLng(latitude, longitude)
map?.cameraPosition = CameraPosition.Builder()
.target(latLng)
.zoom(lastZoomValue)
.build()
symbolManager?.deleteAll()
symbolManager?.create(
SymbolOptions()
.withLatLng(latLng)
.withIconImage(USER_PIN_NAME)
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
)
}
companion object {
const val INITIAL_ZOOM = 12.0
const val USER_PIN_NAME = "USER_PIN_NAME"
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location
import android.content.Context
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import timber.log.Timber
import javax.inject.Inject
class LocationTracker @Inject constructor(
private val context: Context) : LocationListener {
interface Callback {
fun onLocationUpdate(latitude: Double, longitude: Double)
}
private var locationManager: LocationManager? = null
var callback: Callback? = null
fun start() {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
locationManager?.let {
val isGpsEnabled = it.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = it.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
val provider = when {
isGpsEnabled -> LocationManager.GPS_PROVIDER
isNetworkEnabled -> LocationManager.NETWORK_PROVIDER
else -> {
Timber.v("## LocationTracker. There is no location provider available")
return
}
}
// Send last known location without waiting location updates
it.getLastKnownLocation(provider)?.let { lastKnownLocation ->
callback?.onLocationUpdate(lastKnownLocation.latitude, lastKnownLocation.longitude)
}
it.requestLocationUpdates(
provider,
MIN_TIME_MILLIS_TO_UPDATE,
MIN_DISTANCE_METERS_TO_UPDATE,
this
)
} ?: run {
Timber.v("## LocationTracker. LocationManager is not available")
}
}
fun stop() {
locationManager?.removeUpdates(this)
}
override fun onLocationChanged(location: Location) {
callback?.onLocationUpdate(location.latitude, location.longitude)
}
companion object {
const val MIN_TIME_MILLIS_TO_UPDATE = 1 * 60 * 1000L // every 1 minute
const val MIN_DISTANCE_METERS_TO_UPDATE = 10f
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="51dp"
android:height="55dp"
android:viewportWidth="51"
android:viewportHeight="55">
<path
android:pathData="M29.1957,50.7341C41.5276,48.9438 51,38.3281 51,25.5C51,11.4167 39.5833,0 25.5,0C11.4167,0 0,11.4167 0,25.5C0,38.3282 9.4725,48.9439 21.8045,50.7342L25.5001,54.2903L29.1957,50.7341Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
</vector>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -8,4 +9,41 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/shareLocationContainer"
android:layout_width="0dp"
android:layout_height="72dp"
android:background="?android:colorBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/shareLocationImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:background="@drawable/circle"
android:backgroundTint="?colorPrimary"
android:contentDescription="@string/a11y_location_share_icon"
android:padding="10dp"
android:src="@drawable/ic_attachment_location_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
style="@style/TextAppearance.Vector.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/location_share"
android:textColor="?colorPrimary"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/shareLocationImageView"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -3706,4 +3706,6 @@
<!-- Location -->
<string name="location_activity_title_static_sharing">Share location</string>
<string name="location_activity_title_preview">Location</string>
<string name="a11y_location_share_icon">Share location</string>
<string name="location_share">Share location</string>
</resources>