/* * SPDX-FileCopyrightText: 2020, microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.nlp.service import android.app.ActivityManager import android.content.Context import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.location.Location import android.os.Bundle import android.os.IBinder import android.os.SystemClock import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import org.microg.nlp.service.api.* import org.microg.nlp.service.api.Constants.* import java.io.FileDescriptor import java.io.PrintWriter import java.util.* import kotlin.math.max import kotlin.math.min private const val TAG = "LocationService" private const val MIN_LOCATION_INTERVAL = 2500L class LocationService : LifecycleService() { private lateinit var service: LocationServiceImpl override fun onCreate() { super.onCreate() Log.d(TAG, "Creating userspace service...") service = LocationServiceImpl(this, lifecycle) Log.d(TAG, "Created userspace service.") } override fun onBind(intent: Intent): IBinder? { super.onBind(intent) return service.asBinder() } override fun onDestroy() { service.destroy() super.onDestroy() Log.d(TAG, "Destroyed") } override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { service.dump(writer) } } interface LocationReceiver { fun reportLocation(location: Location) } class LocationRequestInternal(private var request: LocationRequest, private val extras: Bundle) { val id: String get() = request.id val callingUid: Int get() = extras.getInt("callingUid") val callingPid: Int get() = extras.getInt("callingPid") val callingPackage: String? get() = extras.getString("callingPackage") val packageName: String get() = extras.getString("packageName")!! val interval: Long get() = request.interval val numUpdates: Int get() = request.numUpdates var updatesDelivered: Int = 0 private set val updatesPending: Int get() = (numUpdates - updatesDelivered).coerceAtLeast(0) val listener: ILocationListener get() = request.listener val source: String get() = extras.getString("source") ?: "" fun report(context: Context, location: Location) { if (updatesPending <= 0) throw IllegalStateException("Not waiting for updates") if (context.checkPermission("android.permission.ACCESS_COARSE_LOCATION", callingPid, callingUid) != PERMISSION_GRANTED) throw SecurityException("No permission to access location") listener.onLocation(STATUS_OK, location) updatesDelivered++ } fun matches(other: LocationRequestInternal): Boolean { if (id == other.id && callingPid == other.callingPid) return true return false } fun adopt(requestInternal: LocationRequestInternal) { updatesDelivered = 0 request = requestInternal.request extras.putAll(requestInternal.extras) } } class LocationServiceImpl(private val context: Context, private val lifecycle: Lifecycle) : ILocationService.Stub(), LifecycleOwner, LocationReceiver { private val requests = arrayListOf() private val fuser = LocationFuser(context, lifecycle, this) private var lastLocation: Location? = null private var interval: Long = 0 private val timer: Timer = Timer("location-requests") private var timerTask: TimerTask? = null private var lastTime: Long = 0 init { lifecycleScope.launchWhenStarted { Log.d(TAG, "Preparing LocationFuser...") fuser.reset() fuser.bind() fuser.update() Log.d(TAG, "Finished preparing LocationFuser") } } private fun updateLocationInterval() { var interval: Long = Long.MAX_VALUE var requestNow = false synchronized(requests) { for (request in requests) { if (request.interval == 0L && request.updatesPending == 1) requestNow = true if (request.interval <= 0 || request.updatesPending <= 0) continue interval = min(interval, request.interval) } } interval = max(interval, MIN_LOCATION_INTERVAL) if (this.interval == interval) return this.interval = interval synchronized(timer) { timerTask?.cancel() timerTask = null if (interval < Long.MAX_VALUE) { Log.d(TAG, "Set merged location interval to $interval") val timerTask = object : TimerTask() { override fun run() { lifecycleScope.launchWhenStarted { lastTime = SystemClock.elapsedRealtime() fuser.update() Log.d(TAG, "Triggered update") } } } val delay = if (requestNow) { 0 } else { (interval - (SystemClock.elapsedRealtime() - lastTime)).coerceIn(0, interval) } timer.scheduleAtFixedRate(timerTask, delay, interval) this.timerTask = timerTask } else { Log.d(TAG, "Disable location updates") } } } private fun getCallingPackage(): String? { val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager val callingPid = getCallingPid() if (manager != null && callingPid > 0) { manager.runningAppProcesses.find { it.pid == callingPid }?.pkgList?.firstOrNull()?.let { return it } } return context.packageManager.getPackagesForUid(getCallingUid())?.firstOrNull() } private fun processOptions(options: Bundle?): Bundle { val options = options ?: Bundle() val callingPackage = getCallingPackage() options.putString("callingPackage", callingPackage) if (!options.containsKey("packageName")) { options.putString("packageName", callingPackage) } else if (context.checkCallingPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED && context.packageName != callingPackage) { val claimedPackageName = options.getString("packageName") if (context.packageManager.getPackagesForUid(getCallingUid())?.any { it == claimedPackageName } != true) { Log.d(TAG, "$callingPackage invalidly claimed package name $claimedPackageName, ignoring") options.putString("packageName", callingPackage) } } options.putInt("callingUid", getCallingUid()) options.putInt("callingPid", getCallingPid()) return options } private fun Bundle.checkPermission(permission: String): Int { return context.checkPermission(permission, getInt("callingPid"), getInt("callingUid")) } override fun getLastLocation(listener: ILocationListener?, options: Bundle?) { val extras = processOptions(options) if (listener == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("android.permission.ACCESS_COARSE_LOCATION") != PERMISSION_GRANTED) return@launchWhenStarted listener.onLocation(STATUS_PERMISSION_ERROR, null) listener.onLocation(STATUS_OK, lastLocation) } } override fun getLastLocationForBackend(packageName: String?, className: String?, signatureDigest: String?, listener: ILocationListener?, options: Bundle?) { val extras = processOptions(options) if (listener == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("android.permission.ACCESS_COARSE_LOCATION") != PERMISSION_GRANTED) return@launchWhenStarted listener.onLocation(STATUS_PERMISSION_ERROR, null) if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) return@launchWhenStarted listener.onLocation(STATUS_PERMISSION_ERROR, null) listener.onLocation(STATUS_OK, fuser.getLastLocationForBackend(packageName, className, signatureDigest)) } } override fun updateLocationRequest(request: LocationRequest?, callback: IStatusCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("android.permission.ACCESS_COARSE_LOCATION") != PERMISSION_GRANTED) return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) if (request == null) return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) val requestInternal = LocationRequestInternal(request, extras) synchronized(requests) { requests.find { it.matches(requestInternal) }?.adopt(requestInternal) ?: requests.add(requestInternal) } updateLocationInterval() callback.onStatus(STATUS_OK) } } override fun cancelLocationRequestByListener(listener: ILocationListener?, callback: IStatusCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (listener == null) return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) synchronized(requests) { requests.removeAll { it.listener == listener } } updateLocationInterval() callback.onStatus(STATUS_OK) } } override fun cancelLocationRequestById(id: String?, callback: IStatusCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (id == null) return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) synchronized(requests) { requests.removeAll { it.id == id && it.callingPid == extras.getInt("callingPid") } } updateLocationInterval() callback.onStatus(STATUS_OK) } } override fun forceLocationUpdate(callback: IStatusCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) fuser.update() callback.onStatus(STATUS_OK) } } override fun reloadPreferences(callback: IStatusCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) fuser.reset() fuser.bind() callback.onStatus(STATUS_OK) } } override fun getLocationBackends(callback: IStringsCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) return@launchWhenStarted callback.onStrings(STATUS_PERMISSION_ERROR, null) callback.onStrings(STATUS_OK, Preferences(context).locationBackends.toList()) } } override fun setLocationBackends(backends: MutableList?, callback: IStatusCallback?, options: Bundle?) { val extras = processOptions(options) if (callback == null || extras.getString("packageName") == null) return lifecycleScope.launchWhenStarted { if (extras.checkPermission("org.microg.nlp.SERVICE_ADMIN") != PERMISSION_GRANTED) return@launchWhenStarted callback.onStatus(STATUS_PERMISSION_ERROR) if (backends == null) return@launchWhenStarted callback.onStatus(STATUS_INVALID_ARGS) Preferences(context).locationBackends = backends.toSet() fuser.reset() fuser.bind() callback.onStatus(STATUS_OK) } } override fun reportLocation(location: Location) { val newLocation = Location(location) this.lastLocation = newLocation val requestsToDelete = hashSetOf() synchronized(requests) { for (request in requests) { try { request.report(context, newLocation) if (request.updatesPending <= 0) requestsToDelete.add(request) } catch (e: Exception) { Log.w(TAG, "Removing request due to error: ", e) requestsToDelete.add(request) } } requests.removeAll(requestsToDelete) } updateLocationInterval() } fun dump(writer: PrintWriter?) { writer?.println("last location: $lastLocation") writer?.println("interval: $interval") writer?.println("${requests.size} requests:") for (request in requests) { writer?.println(" ${request.id} package=${request.packageName} source=${request.source} interval=${request.interval} pending=${request.updatesPending}") } fuser.dump(writer) } fun destroy() { fuser.destroy() } override fun getLifecycle(): Lifecycle = lifecycle }