OsmAnd/OsmAnd-telegram/src/net/osmand/telegram/TelegramService.kt
2021-02-26 18:00:16 +03:00

376 lines
12 KiB
Kotlin

package net.osmand.telegram
import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
import android.location.Location
import android.os.*
import android.util.Log
import android.widget.Toast
import com.google.android.gms.location.*
import net.osmand.PlatformUtil
import net.osmand.telegram.TelegramSettings.ShareChatInfo
import net.osmand.telegram.helpers.TelegramHelper
import net.osmand.telegram.helpers.TelegramHelper.TelegramIncomingMessagesListener
import net.osmand.telegram.helpers.TelegramHelper.TelegramOutgoingMessagesListener
import net.osmand.telegram.notifications.TelegramNotification.NotificationType
import net.osmand.telegram.utils.AndroidUtils
import org.drinkless.td.libcore.telegram.TdApi
private const val UPDATE_WIDGET_INTERVAL_MS = 1000L // 1 sec
private const val UPDATE_LIVE_MESSAGES_INTERVAL_MS = 10000L // 10 sec
private const val UPDATE_LIVE_TRACKS_INTERVAL_MS = 30000L // 30 sec
class TelegramService : Service(), TelegramIncomingMessagesListener,
TelegramOutgoingMessagesListener {
private fun app() = application as TelegramApplication
private val binder = LocationServiceBinder()
private var shouldCleanupResources: Boolean = false
private var updateShareInfoHandler: Handler? = null
private var mHandlerThread = HandlerThread("SharingServiceThread")
private var updateTracksHandler: Handler? = null
private var tracksHandlerThread = HandlerThread("TracksUpdateServiceThread")
private var updateWidgetHandler: Handler? = null
private var updateWidgetThread = HandlerThread("WidgetUpdateServiceThread")
// FusedLocationProviderClient - Main class for receiving location updates.
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
// LocationRequest - Requirements for the location updates, i.e., how often you should receive
// updates, the priority, etc.
private lateinit var locationRequest: LocationRequest
// LocationCallback - Called when FusedLocationProviderClient has a new Location.
private lateinit var locationCallback: LocationCallback
var usedBy = 0
private set
var sendLocationInterval = 0L
private set
private var lastLocationSentTime = 0L
class LocationServiceBinder : Binder()
override fun onCreate() {
super.onCreate()
mHandlerThread.start()
tracksHandlerThread.start()
updateWidgetThread.start()
updateShareInfoHandler = Handler(mHandlerThread.looper)
updateTracksHandler = Handler(tracksHandlerThread.looper)
updateWidgetHandler = Handler(updateWidgetThread.looper)
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
locationRequest = LocationRequest().apply {
// Sets the desired interval for active location updates. This interval is inexact. You
// may not receive updates at all if no location sources are available, or you may
// receive them less frequently than requested. You may also receive updates more
// frequently than requested if other applications are requesting location at a more
// frequent interval.
//
// IMPORTANT NOTE: Apps running on Android 8.0 and higher devices (regardless of
// targetSdkVersion) may receive updates less frequently than this interval when the app
// is no longer in the foreground.
interval = 1000
// Sets the fastest rate for active location updates. This interval is exact, and your
// application will never receive updates more frequently than this value.
//fastestInterval = 500
// Sets the maximum time when batched location updates are delivered. Updates may be
// delivered sooner than this interval.
maxWaitTime = 0
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
super.onLocationResult(locationResult)
val location = convertLocation(locationResult?.lastLocation)
if (System.currentTimeMillis() - lastLocationSentTime > sendLocationInterval * 1000) {
lastLocationSentTime = System.currentTimeMillis()
app().shareLocationHelper.updateLocation(location)
}
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
fun stopIfNeeded(ctx: Context, usageIntent: Int) {
if (usedBy and usageIntent > 0) {
usedBy -= usageIntent
}
when {
usedBy == 0 -> {
shouldCleanupResources = false
val serviceIntent = Intent(ctx, TelegramService::class.java)
ctx.stopService(serviceIntent)
}
isUsedByMyLocation(usedBy) -> {
app().notificationHelper.refreshNotification(NotificationType.LOCATION)
}
isUsedByUsersLocations(usedBy) -> removeLocationUpdates()
}
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val app = app()
val usageIntent = intent.getIntExtra(USAGE_INTENT, 0)
usedBy = usageIntent or usedBy
sendLocationInterval = intent.getLongExtra(SEND_LOCATION_INTERVAL, 0)
app.telegramHelper.addIncomingMessagesListener(this)
app.telegramHelper.addOutgoingMessagesListener(this)
app.telegramService = this
val locationNotification = app.notificationHelper.locationNotification
val notification = app.notificationHelper.buildNotification(locationNotification)
startForeground(locationNotification.telegramNotificationId, notification)
app.notificationHelper.refreshNotification(locationNotification.type)
if (isUsedByMyLocation(usedBy)) {
initLocationUpdates()
startShareInfoUpdates()
startWidgetUpdates()
}
if (isUsedByUsersLocations(usedBy)) {
app.telegramHelper.startLiveMessagesUpdates(app.settings.sendMyLocInterval)
startTracksUpdates()
}
app.shareLocationHelper.checkAndSendBufferMessages()
return START_REDELIVER_INTENT
}
override fun onDestroy() {
super.onDestroy()
val app = app()
app.telegramHelper.stopLiveMessagesUpdates()
app.telegramHelper.removeIncomingMessagesListener(this)
app.telegramHelper.removeOutgoingMessagesListener(this)
app.settings.save()
app.telegramService = null
tracksHandlerThread.quit()
mHandlerThread.quit()
updateWidgetThread.quit()
app().showLocationHelper.addOrUpdateStatusWidget(-1, false)
usedBy = 0
removeLocationUpdates()
if (shouldCleanupResources) {
app.cleanupResources()
}
// remove notification
stopForeground(java.lang.Boolean.TRUE)
}
fun updateSendLocationInterval(newInterval: Long) {
sendLocationInterval = newInterval
}
fun forceLocationUpdate() {
getFirstTimeRunDefaultLocation { location ->
app().shareLocationHelper.updateLocation(location)
}
}
private fun initLocationUpdates() {
getFirstTimeRunDefaultLocation { location ->
app().shareLocationHelper.updateLocation(location)
}
// request location updates
try {
fusedLocationProviderClient.requestLocationUpdates(
locationRequest, locationCallback, Looper.myLooper())
} catch (unlikely: SecurityException) {
Toast.makeText(this, R.string.no_location_permission, Toast.LENGTH_LONG).show()
Log.d(PlatformUtil.TAG, "Lost location permissions. Couldn't request updates. $unlikely")
} catch (e: IllegalArgumentException) {
Toast.makeText(this, R.string.gps_not_available, Toast.LENGTH_LONG).show()
Log.d(PlatformUtil.TAG, "GPS location provider not available")
}
}
private fun startShareInfoUpdates() {
updateShareInfoHandler?.postDelayed({
if (isUsedByMyLocation(usedBy)) {
app().shareLocationHelper.updateSendLiveMessages()
startShareInfoUpdates()
}
}, UPDATE_LIVE_MESSAGES_INTERVAL_MS)
}
private fun startTracksUpdates() {
updateTracksHandler?.postDelayed({
if (isUsedByUsersLocations(usedBy)) {
if (app().settings.hasAnyLiveTracksToShowOnMap()) {
app().showLocationHelper.startUpdateTracksTask()
}
startTracksUpdates()
}
}, UPDATE_LIVE_TRACKS_INTERVAL_MS)
}
private fun startWidgetUpdates() {
updateWidgetHandler?.postDelayed({
if (isUsedByMyLocation(usedBy)) {
val sharingStatus = app().settings.sharingStatusChanges.last()
var isSending = sharingStatus.statusType == TelegramSettings.SharingStatusType.SENDING
val sharingChats = app().settings.getShareLocationChats()
var oldestTime = 0L
if (sharingChats.isNotEmpty() && app().shareLocationHelper.sharingLocation) {
sharingChats.forEach { id ->
val bufferMessages = app().locationMessages.getBufferedMessagesForChat(id)
if (bufferMessages.isNotEmpty()) {
val newTime = bufferMessages[0].time
if (oldestTime == 0L || newTime < oldestTime) {
oldestTime = newTime
}
} else {
oldestTime = 0L
}
}
} else {
isSending = false
oldestTime = -1
}
app().showLocationHelper.addOrUpdateStatusWidget(oldestTime, isSending)
} else {
app().showLocationHelper.addOrUpdateStatusWidget(-1, false)
}
startWidgetUpdates()
}, UPDATE_WIDGET_INTERVAL_MS)
}
@SuppressLint("MissingPermission")
private fun getFirstTimeRunDefaultLocation(locationListener: (net.osmand.Location?) -> Unit) {
val app = app()
if (!AndroidUtils.isLocationPermissionAvailable(app)) {
locationListener(null)
return
}
fusedLocationProviderClient.lastLocation
.addOnSuccessListener { location : Location? ->
locationListener(convertLocation(location))
}
}
private fun removeLocationUpdates() {
// remove updates
try {
fusedLocationProviderClient.removeLocationUpdates(locationCallback)
} catch (unlikely: SecurityException) {
Log.d(PlatformUtil.TAG, "Lost location permissions. Couldn't remove updates. $unlikely")
}
}
override fun onTaskRemoved(rootIntent: Intent) {
val app = app()
if (app.telegramService != null) {
shouldCleanupResources = true
// Do not stop service after UI task was dismissed
//this@TelegramService.stopSelf()
}
}
override fun onReceiveChatLocationMessages(chatId: Long, vararg messages: TdApi.Message) {
app().showLocationHelper.startShowMessagesTask(chatId, *messages)
messages.forEach {
if (!it.isOutgoing) {
app().locationMessages.addNewLocationMessage(it)
}
}
}
override fun onDeleteChatLocationMessages(chatId: Long, messages: List<TdApi.Message>) {
app().showLocationHelper.startDeleteMessagesTask(chatId, messages)
}
override fun updateLocationMessages() {
app().showLocationHelper.startUpdateMessagesTask()
}
override fun onUpdateMessages(messages: List<TdApi.Message>) {
messages.forEach {
app().settings.updateShareInfo(it)
app().shareLocationHelper.checkAndSendBufferMessagesToChat(it.chatId)
if (it.sendingState == null && !it.isOutgoing && (it.content is TdApi.MessageLocation || it.content is TdApi.MessageText)) {
app().locationMessages.addNewLocationMessage(it)
}
}
}
override fun onDeleteMessages(chatId: Long, messages: List<Long>) {
app().settings.onDeleteLiveMessages(chatId, messages)
}
override fun onSendLiveLocationError(code: Int, message: String, shareInfo: ShareChatInfo, messageType: Int) {
Log.d(PlatformUtil.TAG, "Send live location error: $code - $message")
when (messageType) {
TelegramHelper.MESSAGE_TYPE_TEXT -> shareInfo.pendingTdLibText--
TelegramHelper.MESSAGE_TYPE_MAP -> {
shareInfo.pendingTdLibMap--
shareInfo.currentMapMessageId = -1L
}
}
}
companion object {
const val USED_BY_MY_LOCATION: Int = 1
const val USED_BY_USERS_LOCATIONS: Int = 2
const val USAGE_INTENT = "SERVICE_USED_BY"
const val SEND_LOCATION_INTERVAL = "SEND_LOCATION_INTERVAL"
fun isUsedByMyLocation(usedBy: Int): Boolean {
return (usedBy and USED_BY_MY_LOCATION) > 0
}
fun isUsedByUsersLocations(usedBy: Int): Boolean {
return (usedBy and USED_BY_USERS_LOCATIONS) > 0
}
fun convertLocation(l: Location?): net.osmand.Location? {
if (l == null) {
return null
}
val r = net.osmand.Location(l.provider)
r.latitude = l.latitude
r.longitude = l.longitude
r.time = l.time
if (l.hasAccuracy()) {
r.accuracy = l.accuracy
}
if (l.hasSpeed()) {
r.speed = l.speed
}
if (l.hasAltitude()) {
r.altitude = l.altitude
}
if (l.hasBearing()) {
r.bearing = l.bearing
}
if (l.hasAltitude()) {
r.altitude = l.altitude
}
return r
}
}
}