mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge pull request #5595 from vector-im/feature/ons/live_location_service
Live Location Sharing - Foreground Service
This commit is contained in:
commit
08476a91e4
1
changelog.d/5595.feature
Normal file
1
changelog.d/5595.feature
Normal file
@ -0,0 +1 @@
|
||||
Live Location Sharing - Foreground Service and Notification
|
@ -84,8 +84,8 @@
|
||||
android:resizeableActivity="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Vector.Light"
|
||||
android:taskAffinity="${applicationId}.${appTaskAffinitySuffix}"
|
||||
android:theme="@style/Theme.Vector.Light"
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<!-- No limit for screen ratio: avoid black strips -->
|
||||
@ -369,6 +369,11 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".features.location.LocationSharingService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<receiver
|
||||
|
@ -23,5 +23,5 @@ sealed class LocationSharingAction : VectorViewModelAction {
|
||||
data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
|
||||
data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
|
||||
object ZoomToUserLocation : LocationSharingAction()
|
||||
object StartLiveLocationSharing : LocationSharingAction()
|
||||
data class StartLiveLocationSharing(val duration: Long) : LocationSharingAction()
|
||||
}
|
||||
|
@ -16,11 +16,13 @@
|
||||
|
||||
package im.vector.app.features.location
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
@ -82,9 +84,10 @@ class LocationSharingFragment @Inject constructor(
|
||||
|
||||
viewModel.observeViewEvents {
|
||||
when (it) {
|
||||
LocationSharingViewEvents.Close -> locationSharingNavigator.quit()
|
||||
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
|
||||
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
|
||||
LocationSharingViewEvents.Close -> locationSharingNavigator.quit()
|
||||
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
|
||||
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
|
||||
is LocationSharingViewEvents.StartLiveLocationService -> handleStartLiveLocationService(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -176,6 +179,16 @@ class LocationSharingFragment @Inject constructor(
|
||||
views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
|
||||
}
|
||||
|
||||
private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) {
|
||||
val args = LocationSharingService.RoomArgs(event.sessionId, event.roomId, event.duration)
|
||||
|
||||
Intent(requireContext(), LocationSharingService::class.java)
|
||||
.putExtra(LocationSharingService.EXTRA_ROOM_ARGS, args)
|
||||
.also {
|
||||
ContextCompat.startForegroundService(requireContext(), it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initOptionsPicker() {
|
||||
// set no option at start
|
||||
views.shareLocationOptionsPicker.render()
|
||||
@ -221,7 +234,9 @@ class LocationSharingFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun startLiveLocationSharing() {
|
||||
viewModel.handle(LocationSharingAction.StartLiveLocationSharing)
|
||||
// TODO. Get duration from user
|
||||
val duration = 30 * 1000L
|
||||
viewModel.handle(LocationSharingAction.StartLiveLocationSharing(duration))
|
||||
}
|
||||
|
||||
private fun updateMap(state: LocationSharingViewState) {
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2022 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.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.core.services.VectorService
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocationSharingService : VectorService(), LocationTracker.Callback {
|
||||
|
||||
@Parcelize
|
||||
data class RoomArgs(
|
||||
val sessionId: String,
|
||||
val roomId: String,
|
||||
val durationMillis: Long
|
||||
) : Parcelable
|
||||
|
||||
@Inject lateinit var notificationUtils: NotificationUtils
|
||||
@Inject lateinit var locationTracker: LocationTracker
|
||||
|
||||
private var roomArgsList = mutableListOf<RoomArgs>()
|
||||
private var timers = mutableListOf<Timer>()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.i("### LocationSharingService.onCreate")
|
||||
|
||||
// Start tracking location
|
||||
locationTracker.addCallback(this)
|
||||
locationTracker.start()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val roomArgs = intent?.getParcelableExtra(EXTRA_ROOM_ARGS) as? RoomArgs
|
||||
|
||||
Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
|
||||
|
||||
if (roomArgs != null) {
|
||||
roomArgsList.add(roomArgs)
|
||||
|
||||
// Show a sticky notification
|
||||
val notification = notificationUtils.buildLiveLocationSharingNotification()
|
||||
startForeground(roomArgs.roomId.hashCode(), notification)
|
||||
|
||||
// Schedule a timer to stop sharing
|
||||
scheduleTimer(roomArgs.roomId, roomArgs.durationMillis)
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun scheduleTimer(roomId: String, durationMillis: Long) {
|
||||
Timer()
|
||||
.apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
stopSharingLocation(roomId)
|
||||
timers.remove(this@apply)
|
||||
}
|
||||
}, durationMillis)
|
||||
}
|
||||
.also {
|
||||
timers.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopSharingLocation(roomId: String) {
|
||||
Timber.i("### LocationSharingService.stopSharingLocation for $roomId")
|
||||
synchronized(roomArgsList) {
|
||||
roomArgsList.removeAll { it.roomId == roomId }
|
||||
if (roomArgsList.isEmpty()) {
|
||||
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
|
||||
destroyMe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationUpdate(locationData: LocationData) {
|
||||
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
|
||||
}
|
||||
|
||||
override fun onLocationProviderIsNotAvailable() {
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun destroyMe() {
|
||||
locationTracker.removeCallback(this)
|
||||
timers.forEach { it.cancel() }
|
||||
timers.clear()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.i("### LocationSharingService.onDestroy")
|
||||
destroyMe()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ROOM_ARGS = "EXTRA_ROOM_ARGS"
|
||||
}
|
||||
}
|
@ -22,4 +22,5 @@ sealed class LocationSharingViewEvents : VectorViewEvents {
|
||||
object Close : LocationSharingViewEvents()
|
||||
object LocationNotAvailableError : LocationSharingViewEvents()
|
||||
data class ZoomToUserLocation(val userLocation: LocationData) : LocationSharingViewEvents()
|
||||
data class StartLiveLocationService(val sessionId: String, val roomId: String, val duration: Long) : LocationSharingViewEvents()
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Sampling period to compare target location and user location.
|
||||
@ -64,7 +63,8 @@ class LocationSharingViewModel @AssistedInject constructor(
|
||||
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
locationTracker.start(this)
|
||||
locationTracker.addCallback(this)
|
||||
locationTracker.start()
|
||||
setUserItem()
|
||||
updatePin()
|
||||
compareTargetAndUserLocation()
|
||||
@ -111,16 +111,16 @@ class LocationSharingViewModel @AssistedInject constructor(
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
locationTracker.stop()
|
||||
locationTracker.removeCallback(this)
|
||||
}
|
||||
|
||||
override fun handle(action: LocationSharingAction) {
|
||||
when (action) {
|
||||
LocationSharingAction.CurrentUserLocationSharing -> handleCurrentUserLocationSharingAction()
|
||||
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
|
||||
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
|
||||
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
|
||||
LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction()
|
||||
LocationSharingAction.CurrentUserLocationSharing -> handleCurrentUserLocationSharingAction()
|
||||
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
|
||||
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
|
||||
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
|
||||
is LocationSharingAction.StartLiveLocationSharing -> handleStartLiveLocationSharingAction(action.duration)
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,9 +158,12 @@ class LocationSharingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartLiveLocationSharingAction() {
|
||||
// TODO start sharing live location and update view state
|
||||
Timber.d("live location sharing started")
|
||||
private fun handleStartLiveLocationSharingAction(duration: Long) {
|
||||
_viewEvents.post(LocationSharingViewEvents.StartLiveLocationService(
|
||||
sessionId = session.sessionId,
|
||||
roomId = room.roomId,
|
||||
duration = duration
|
||||
))
|
||||
}
|
||||
|
||||
override fun onLocationUpdate(locationData: LocationData) {
|
||||
|
@ -26,7 +26,9 @@ import androidx.core.location.LocationListenerCompat
|
||||
import im.vector.app.BuildConfig
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class LocationTracker @Inject constructor(
|
||||
context: Context
|
||||
) : LocationListenerCompat {
|
||||
@ -38,18 +40,17 @@ class LocationTracker @Inject constructor(
|
||||
fun onLocationProviderIsNotAvailable()
|
||||
}
|
||||
|
||||
private var callback: Callback? = null
|
||||
private var callbacks = mutableListOf<Callback>()
|
||||
|
||||
private var hasGpsProviderLiveLocation = false
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||
fun start(callback: Callback?) {
|
||||
fun start() {
|
||||
Timber.d("## LocationTracker. start()")
|
||||
hasGpsProviderLiveLocation = false
|
||||
this.callback = callback
|
||||
|
||||
if (locationManager == null) {
|
||||
callback?.onLocationProviderIsNotAvailable()
|
||||
callbacks.forEach { it.onLocationProviderIsNotAvailable() }
|
||||
Timber.v("## LocationTracker. LocationManager is not available")
|
||||
return
|
||||
}
|
||||
@ -79,7 +80,7 @@ class LocationTracker @Inject constructor(
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
callback?.onLocationProviderIsNotAvailable()
|
||||
callbacks.forEach { it.onLocationProviderIsNotAvailable() }
|
||||
Timber.v("## LocationTracker. There is no location provider available")
|
||||
}
|
||||
}
|
||||
@ -88,7 +89,20 @@ class LocationTracker @Inject constructor(
|
||||
fun stop() {
|
||||
Timber.d("## LocationTracker. stop()")
|
||||
locationManager?.removeUpdates(this)
|
||||
callback = null
|
||||
callbacks.clear()
|
||||
}
|
||||
|
||||
fun addCallback(callback: Callback) {
|
||||
if (!callbacks.contains(callback)) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCallback(callback: Callback) {
|
||||
callbacks.remove(callback)
|
||||
if (callbacks.size == 0) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
@ -113,12 +127,12 @@ class LocationTracker @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.onLocationUpdate(location.toLocationData())
|
||||
callbacks.forEach { it.onLocationUpdate(location.toLocationData()) }
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
Timber.d("## LocationTracker. onProviderDisabled: $provider")
|
||||
callback?.onLocationProviderIsNotAvailable()
|
||||
callbacks.forEach { it.onLocationProviderIsNotAvailable() }
|
||||
}
|
||||
|
||||
private fun Location.toLocationData(): LocationData {
|
||||
|
@ -521,6 +521,20 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification that indicates the application is retrieving location even if it is in background or killed.
|
||||
*/
|
||||
fun buildLiveLocationSharingNotification(): Notification {
|
||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(stringProvider.getString(R.string.live_location_sharing_notification_title))
|
||||
.setContentText(stringProvider.getString(R.string.live_location_sharing_notification_description))
|
||||
.setSmallIcon(R.drawable.ic_attachment_location_live_white)
|
||||
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||
.setCategory(NotificationCompat.CATEGORY_LOCATION_SHARING)
|
||||
.setContentIntent(buildOpenHomePendingIntentForSummary())
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
|
||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setGroup(stringProvider.getString(R.string.app_name))
|
||||
|
@ -2950,6 +2950,8 @@
|
||||
<string name="location_timeline_failed_to_load_map">Failed to load map</string>
|
||||
<string name="location_share_live_enabled">Live location enabled</string>
|
||||
<string name="location_share_live_stop">Stop</string>
|
||||
<string name="live_location_sharing_notification_title">${app_name} Live Location</string>
|
||||
<string name="live_location_sharing_notification_description">Location sharing is in progress</string>
|
||||
|
||||
<string name="message_bubbles">Show Message bubbles</string>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user