-
Notifications
You must be signed in to change notification settings - Fork 741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Live Location Sharing - Foreground Service #5595
Changes from 12 commits
a1d2794
c63fc3d
3343680
7e5c293
70c8a8b
3fa4aea
96a2bc9
f18a107
5f74442
7a575ed
79afdf7
7285bc6
bdbdfe5
9b271e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Live Location Sharing - Foreground Service |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* 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>() | ||
|
||
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().schedule(object : TimerTask() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we might also want to keep a reference to the timer and cancel it if it's still running when the service is destroyed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch, done. |
||
override fun run() { | ||
stopSharingLocation(roomId) | ||
} | ||
}, durationMillis) | ||
} | ||
|
||
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) | ||
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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. now that we're calling locationManager.requestLocationUpdates(
provider,
MIN_TIME_TO_UPDATE_LOCATION_MILLIS,
MIN_DISTANCE_TO_UPDATE_LOCATION_METERS,
this
) to avoid registering duplicate updates? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is protected internally and doesn't create multiple requests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good to know, thanks! |
||
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 { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be worth a constant?
not for this PR - thinking out of loud about todos I'm wondering if they are helpful or add noise, would a
User can set live location duration
ticket that references the class provide the same information 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already started implementing time-limiting UI. I will create another quick PR today.