mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge d2e345842083fc61526fa903112c59006399ab68 into c127eb83ab94c069c32d37530d2faecd381cd2a8
This commit is contained in:
commit
295bc7965c
@ -17,14 +17,32 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
|
||||
private const val TAG = "FusedLocationProvider"
|
||||
}
|
||||
|
||||
private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
|
||||
|
||||
// Map to keep track of callbacks to remove them later
|
||||
private val activeCallbacks = mutableMapOf<(Location) -> Unit, LocationCallback>()
|
||||
private val activeCurrentLocationRequests = mutableSetOf<CancellationTokenSource>()
|
||||
private val fusedLocationClient: FusedLocationProviderClient =
|
||||
LocationServices.getFusedLocationProviderClient(context)
|
||||
private val systemFallback = SystemLocationProvider(context)
|
||||
|
||||
private val activeCallbacks = mutableMapOf<(Location) -> Unit, UpdateRegistration>()
|
||||
private val activeOneShotRequests = mutableSetOf<PendingOneShot>()
|
||||
private val activeLastKnownRequests = mutableSetOf<PendingLastKnown>()
|
||||
|
||||
private class PendingOneShot {
|
||||
val cancellation = CancellationTokenSource()
|
||||
var fallbackStarted = false
|
||||
}
|
||||
|
||||
private class PendingLastKnown {
|
||||
var fallbackStarted = false
|
||||
}
|
||||
|
||||
private class UpdateRegistration(
|
||||
val fusedCallback: LocationCallback,
|
||||
val systemCallback: (Location) -> Unit
|
||||
) {
|
||||
var fallbackStarted = false
|
||||
}
|
||||
|
||||
private fun hasLocationPermission(): Boolean {
|
||||
return LiveLocationPrivacyGate.isEnabled &&
|
||||
return LiveLocationPrivacyGate.captureToken() != null &&
|
||||
(ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
@ -37,18 +55,76 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
|
||||
return
|
||||
}
|
||||
|
||||
val pending = PendingLastKnown()
|
||||
synchronized(activeLastKnownRequests) {
|
||||
activeLastKnownRequests += pending
|
||||
}
|
||||
|
||||
try {
|
||||
fusedLocationClient.lastLocation
|
||||
.addOnSuccessListener { location ->
|
||||
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
|
||||
if (location != null && hasLocationPermission()) {
|
||||
completeLastKnown(pending, callback, location)
|
||||
} else {
|
||||
startSystemLastKnownFallback(pending, callback)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
.addOnFailureListener {
|
||||
Log.e(TAG, "Error getting last-known fused location")
|
||||
callback(null)
|
||||
startSystemLastKnownFallback(pending, callback)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception getting last-known fused location")
|
||||
callback(null)
|
||||
Log.e(TAG, "Exception getting last-known fused location", e)
|
||||
startSystemLastKnownFallback(pending, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSystemLastKnownFallback(
|
||||
pending: PendingLastKnown,
|
||||
callback: (Location?) -> Unit
|
||||
) {
|
||||
var deliverNull = false
|
||||
var shouldStartFallback = false
|
||||
|
||||
synchronized(activeLastKnownRequests) {
|
||||
when {
|
||||
!activeLastKnownRequests.contains(pending) -> return
|
||||
!hasLocationPermission() -> {
|
||||
activeLastKnownRequests.remove(pending)
|
||||
deliverNull = true
|
||||
}
|
||||
!pending.fallbackStarted -> {
|
||||
pending.fallbackStarted = true
|
||||
shouldStartFallback = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
deliverNull -> callback(null)
|
||||
shouldStartFallback -> {
|
||||
try {
|
||||
systemFallback.getLastKnownLocation { location ->
|
||||
completeLastKnown(pending, callback, location)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "System last-known location fallback failed", e)
|
||||
completeLastKnown(pending, callback, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeLastKnown(
|
||||
pending: PendingLastKnown,
|
||||
callback: (Location?) -> Unit,
|
||||
location: Location?
|
||||
) {
|
||||
val shouldDeliver = synchronized(activeLastKnownRequests) {
|
||||
activeLastKnownRequests.remove(pending)
|
||||
}
|
||||
if (shouldDeliver) {
|
||||
callback(location.takeIf { hasLocationPermission() })
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,33 +135,82 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
|
||||
return
|
||||
}
|
||||
|
||||
val pending = PendingOneShot()
|
||||
synchronized(activeOneShotRequests) {
|
||||
activeOneShotRequests += pending
|
||||
}
|
||||
|
||||
try {
|
||||
val request = CurrentLocationRequest.Builder()
|
||||
.setPriority(Priority.PRIORITY_HIGH_ACCURACY)
|
||||
.setDurationMillis(30000)
|
||||
.setDurationMillis(30_000L)
|
||||
.build()
|
||||
val cancellation = CancellationTokenSource()
|
||||
|
||||
synchronized(activeCurrentLocationRequests) {
|
||||
activeCurrentLocationRequests.add(cancellation)
|
||||
}
|
||||
|
||||
fusedLocationClient.getCurrentLocation(request, cancellation.token)
|
||||
fusedLocationClient.getCurrentLocation(request, pending.cancellation.token)
|
||||
.addOnSuccessListener { location ->
|
||||
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.e(TAG, "Error getting fresh fused location")
|
||||
callback(null)
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
synchronized(activeCurrentLocationRequests) {
|
||||
activeCurrentLocationRequests.remove(cancellation)
|
||||
if (location != null && hasLocationPermission()) {
|
||||
completeOneShot(pending, callback, location)
|
||||
} else {
|
||||
startSystemOneShotFallback(pending, callback)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
Log.w(TAG, "Fused fresh location failed; using system fallback")
|
||||
startSystemOneShotFallback(pending, callback)
|
||||
}
|
||||
.addOnCanceledListener {
|
||||
startSystemOneShotFallback(pending, callback)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception getting fresh fused location")
|
||||
callback(null)
|
||||
Log.w(TAG, "Fused fresh location could not start; using system fallback", e)
|
||||
startSystemOneShotFallback(pending, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSystemOneShotFallback(
|
||||
pending: PendingOneShot,
|
||||
callback: (Location?) -> Unit
|
||||
) {
|
||||
var deliverNull = false
|
||||
var fallbackStartFailed = false
|
||||
|
||||
synchronized(activeOneShotRequests) {
|
||||
if (!activeOneShotRequests.contains(pending)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasLocationPermission()) {
|
||||
activeOneShotRequests.remove(pending)
|
||||
deliverNull = true
|
||||
} else if (!pending.fallbackStarted) {
|
||||
pending.fallbackStarted = true
|
||||
try {
|
||||
systemFallback.requestFreshLocation { location ->
|
||||
completeOneShot(pending, callback, location)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "System location fallback could not start", e)
|
||||
fallbackStartFailed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
deliverNull -> callback(null)
|
||||
fallbackStartFailed -> completeOneShot(pending, callback, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeOneShot(
|
||||
pending: PendingOneShot,
|
||||
callback: (Location?) -> Unit,
|
||||
location: Location?
|
||||
) {
|
||||
val shouldDeliver = synchronized(activeOneShotRequests) {
|
||||
activeOneShotRequests.remove(pending)
|
||||
}
|
||||
if (shouldDeliver) {
|
||||
callback(location.takeIf { hasLocationPermission() })
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,66 +222,132 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
|
||||
) {
|
||||
if (!hasLocationPermission()) return
|
||||
|
||||
removeLocationUpdates(callback)
|
||||
|
||||
lateinit var registration: UpdateRegistration
|
||||
val fusedCallback = object : LocationCallback() {
|
||||
override fun onLocationResult(result: LocationResult) {
|
||||
if (isCurrentRegistration(callback, registration) && hasLocationPermission()) {
|
||||
result.lastLocation?.let(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
val systemCallback: (Location) -> Unit = { location ->
|
||||
if (isCurrentRegistration(callback, registration) && hasLocationPermission()) {
|
||||
callback(location)
|
||||
}
|
||||
}
|
||||
registration = UpdateRegistration(
|
||||
fusedCallback = fusedCallback,
|
||||
systemCallback = systemCallback
|
||||
)
|
||||
|
||||
try {
|
||||
val request = LocationRequest.Builder(intervalMs)
|
||||
.setMinUpdateDistanceMeters(minDistanceMeters)
|
||||
.setPriority(Priority.PRIORITY_HIGH_ACCURACY)
|
||||
.build()
|
||||
|
||||
val locationCallback = object : LocationCallback() {
|
||||
override fun onLocationResult(result: LocationResult) {
|
||||
if (LiveLocationPrivacyGate.isEnabled) {
|
||||
result.lastLocation?.let { callback(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(activeCallbacks) {
|
||||
activeCallbacks[callback] = locationCallback
|
||||
activeCallbacks[callback] = registration
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
request,
|
||||
fusedCallback,
|
||||
Looper.getMainLooper()
|
||||
)
|
||||
.addOnSuccessListener {
|
||||
Log.d(TAG, "Registered fused updates")
|
||||
}
|
||||
.addOnFailureListener {
|
||||
Log.w(TAG, "Fused updates unavailable; using system location provider")
|
||||
startSystemUpdatesFallback(
|
||||
callback = callback,
|
||||
registration = registration,
|
||||
intervalMs = intervalMs,
|
||||
minDistanceMeters = minDistanceMeters
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
request,
|
||||
locationCallback,
|
||||
Looper.getMainLooper()
|
||||
)
|
||||
Log.d(TAG, "Registered fused updates")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error requesting fused updates")
|
||||
Log.w(TAG, "Unable to register fused updates; using system location provider", e)
|
||||
startSystemUpdatesFallback(
|
||||
callback = callback,
|
||||
registration = registration,
|
||||
intervalMs = intervalMs,
|
||||
minDistanceMeters = minDistanceMeters
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeLocationUpdates(callback: (Location) -> Unit) {
|
||||
try {
|
||||
val locationCallback = synchronized(activeCallbacks) {
|
||||
private fun startSystemUpdatesFallback(
|
||||
callback: (Location) -> Unit,
|
||||
registration: UpdateRegistration,
|
||||
intervalMs: Long,
|
||||
minDistanceMeters: Float
|
||||
) {
|
||||
synchronized(activeCallbacks) {
|
||||
if (activeCallbacks[callback] !== registration || registration.fallbackStarted) {
|
||||
return
|
||||
}
|
||||
if (!hasLocationPermission()) {
|
||||
activeCallbacks.remove(callback)
|
||||
return
|
||||
}
|
||||
|
||||
if (locationCallback != null) {
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback)
|
||||
Log.d(TAG, "Removed fused updates")
|
||||
registration.fallbackStarted = true
|
||||
try {
|
||||
systemFallback.requestLocationUpdates(
|
||||
intervalMs = intervalMs,
|
||||
minDistanceMeters = minDistanceMeters,
|
||||
callback = registration.systemCallback
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to register system location fallback", e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error removing fused updates")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCurrentRegistration(
|
||||
callback: (Location) -> Unit,
|
||||
registration: UpdateRegistration
|
||||
): Boolean = synchronized(activeCallbacks) {
|
||||
activeCallbacks[callback] === registration
|
||||
}
|
||||
|
||||
override fun removeLocationUpdates(callback: (Location) -> Unit) {
|
||||
val registration = synchronized(activeCallbacks) {
|
||||
activeCallbacks.remove(callback)?.also {
|
||||
if (it.fallbackStarted) {
|
||||
systemFallback.removeLocationUpdates(it.systemCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (registration != null) {
|
||||
runCatching { fusedLocationClient.removeLocationUpdates(registration.fusedCallback) }
|
||||
.onFailure { Log.w(TAG, "Unable to remove fused updates", it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
try {
|
||||
synchronized(activeCallbacks) {
|
||||
for ((_, locationCallback) in activeCallbacks) {
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback)
|
||||
}
|
||||
activeCallbacks.clear()
|
||||
}
|
||||
synchronized(activeCurrentLocationRequests) {
|
||||
activeCurrentLocationRequests.forEach { it.cancel() }
|
||||
activeCurrentLocationRequests.clear()
|
||||
}
|
||||
Log.d(TAG, "Cancelled all fused updates")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error cancelling fused provider")
|
||||
val registrations = synchronized(activeCallbacks) {
|
||||
activeCallbacks.values.toList().also { activeCallbacks.clear() }
|
||||
}
|
||||
registrations.forEach { registration ->
|
||||
runCatching { fusedLocationClient.removeLocationUpdates(registration.fusedCallback) }
|
||||
.onFailure { Log.w(TAG, "Unable to remove fused updates", it) }
|
||||
}
|
||||
|
||||
synchronized(activeOneShotRequests) {
|
||||
activeOneShotRequests.forEach { it.cancellation.cancel() }
|
||||
activeOneShotRequests.clear()
|
||||
}
|
||||
synchronized(activeLastKnownRequests) {
|
||||
activeLastKnownRequests.clear()
|
||||
}
|
||||
// This instance owns the fallback provider, so it is safe to cancel all of its work here.
|
||||
systemFallback.cancel()
|
||||
Log.d(TAG, "Cancelled all fused location requests")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
||||
@ -17,19 +18,41 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SystemLocationProvider"
|
||||
private const val FRESH_LOCATION_TIMEOUT_MS = 30_000L
|
||||
}
|
||||
|
||||
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
|
||||
// Map to keep track of listeners to unregister them later
|
||||
|
||||
// Map to keep track of listeners to unregister them later.
|
||||
private val activeListeners = mutableMapOf<(Location) -> Unit, LocationListener>()
|
||||
private val activeOneShotListeners = mutableMapOf<(Location?) -> Unit, LocationListener>()
|
||||
private val activeOneShotRunnables = mutableMapOf<(Location?) -> Unit, Runnable>()
|
||||
private val activeOneShotCancellationSignals = mutableMapOf<(Location?) -> Unit, CancellationSignal>()
|
||||
private val activeOneShotRequests = mutableSetOf<PendingOneShot>()
|
||||
|
||||
private class PendingOneShot(
|
||||
val callback: (Location?) -> Unit,
|
||||
val providers: List<String>,
|
||||
val deadlineElapsedRealtime: Long
|
||||
) {
|
||||
var nextProviderIndex = 0
|
||||
var cancellationSignal: CancellationSignal? = null
|
||||
var listener: LocationListener? = null
|
||||
var timeoutRunnable: Runnable? = null
|
||||
var completed = false
|
||||
}
|
||||
|
||||
private data class ProviderAttempt(
|
||||
val provider: String,
|
||||
val timeoutMs: Long
|
||||
)
|
||||
|
||||
private data class OneShotResources(
|
||||
val cancellationSignal: CancellationSignal?,
|
||||
val listener: LocationListener?,
|
||||
val timeoutRunnable: Runnable?
|
||||
)
|
||||
|
||||
private fun hasLocationPermission(): Boolean {
|
||||
return LiveLocationPrivacyGate.isEnabled &&
|
||||
return LiveLocationPrivacyGate.captureToken() != null &&
|
||||
(ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
@ -53,7 +76,7 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(bestLocation.takeIf { LiveLocationPrivacyGate.isEnabled })
|
||||
callback(bestLocation.takeIf { hasLocationPermission() })
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting last-known location")
|
||||
callback(null)
|
||||
@ -67,100 +90,279 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val providers = listOf(
|
||||
val request = PendingOneShot(
|
||||
callback = callback,
|
||||
providers = listOf(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
LocationManager.PASSIVE_PROVIDER
|
||||
)
|
||||
LocationManager.NETWORK_PROVIDER
|
||||
),
|
||||
deadlineElapsedRealtime = SystemClock.elapsedRealtime() + FRESH_LOCATION_TIMEOUT_MS
|
||||
)
|
||||
|
||||
var providerFound = false
|
||||
for (provider in providers) {
|
||||
if (locationManager.isProviderEnabled(provider)) {
|
||||
Log.d(TAG, "Requesting fresh location from $provider")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val cancellationSignal = CancellationSignal()
|
||||
synchronized(activeOneShotCancellationSignals) {
|
||||
activeOneShotCancellationSignals[callback] = cancellationSignal
|
||||
}
|
||||
try {
|
||||
locationManager.getCurrentLocation(
|
||||
provider,
|
||||
cancellationSignal,
|
||||
context.mainExecutor
|
||||
) { location ->
|
||||
synchronized(activeOneShotCancellationSignals) {
|
||||
activeOneShotCancellationSignals.remove(callback)
|
||||
}
|
||||
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
synchronized(activeOneShotCancellationSignals) {
|
||||
activeOneShotCancellationSignals.remove(callback)
|
||||
}
|
||||
cancellationSignal.cancel()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
// For older versions, use requestSingleUpdate with timeout mechanism
|
||||
val timeoutRunnable = Runnable {
|
||||
Log.w(TAG, "Location request timed out")
|
||||
synchronized(activeOneShotListeners) {
|
||||
val listener = activeOneShotListeners.remove(callback)
|
||||
activeOneShotRunnables.remove(callback)
|
||||
if (listener != null) {
|
||||
try {
|
||||
locationManager.removeUpdates(listener)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error removing timed-out listener")
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(null)
|
||||
}
|
||||
synchronized(activeOneShotRequests) {
|
||||
activeOneShotRequests += request
|
||||
}
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
synchronized(activeOneShotListeners) {
|
||||
activeOneShotListeners.remove(callback)
|
||||
val runnable = activeOneShotRunnables.remove(callback)
|
||||
if (runnable != null) {
|
||||
handler.removeCallbacks(runnable)
|
||||
}
|
||||
}
|
||||
try {
|
||||
locationManager.removeUpdates(this)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error removing updates in callback")
|
||||
}
|
||||
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
|
||||
}
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
override fun onProviderDisabled(provider: String) {}
|
||||
private fun startNextOneShotProvider(request: PendingOneShot) {
|
||||
var shouldFinish = false
|
||||
val attempt = synchronized(activeOneShotRequests) {
|
||||
if (!isOneShotActiveLocked(request)) {
|
||||
null
|
||||
} else if (!hasLocationPermission()) {
|
||||
shouldFinish = true
|
||||
null
|
||||
} else {
|
||||
val remainingMs = request.deadlineElapsedRealtime - SystemClock.elapsedRealtime()
|
||||
if (remainingMs <= 0L) {
|
||||
shouldFinish = true
|
||||
null
|
||||
} else {
|
||||
var nextAttempt: ProviderAttempt? = null
|
||||
while (request.nextProviderIndex < request.providers.size && nextAttempt == null) {
|
||||
val provider = request.providers[request.nextProviderIndex++]
|
||||
if (isProviderEnabled(provider)) {
|
||||
val remainingEnabledProviders = 1 + request.providers
|
||||
.drop(request.nextProviderIndex)
|
||||
.count(::isProviderEnabled)
|
||||
nextAttempt = ProviderAttempt(
|
||||
provider = provider,
|
||||
timeoutMs = maxOf(1L, remainingMs / remainingEnabledProviders)
|
||||
)
|
||||
}
|
||||
|
||||
synchronized(activeOneShotListeners) {
|
||||
activeOneShotListeners[callback] = listener
|
||||
activeOneShotRunnables[callback] = timeoutRunnable
|
||||
}
|
||||
|
||||
locationManager.requestSingleUpdate(provider, listener, null)
|
||||
handler.postDelayed(timeoutRunnable, 30000L) // 30s timeout
|
||||
}
|
||||
providerFound = true
|
||||
break
|
||||
if (nextAttempt == null) {
|
||||
shouldFinish = true
|
||||
}
|
||||
nextAttempt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
attempt != null -> requestOneShotFromProvider(request, attempt)
|
||||
shouldFinish -> finishOneShot(request, null)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun requestOneShotFromProvider(
|
||||
request: PendingOneShot,
|
||||
attempt: ProviderAttempt
|
||||
) {
|
||||
Log.d(TAG, "Requesting fresh location from ${attempt.provider}")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
requestCurrentLocation(request, attempt)
|
||||
} else {
|
||||
requestSingleLocationUpdate(request, attempt)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun requestCurrentLocation(
|
||||
request: PendingOneShot,
|
||||
attempt: ProviderAttempt
|
||||
) {
|
||||
val cancellationSignal = CancellationSignal()
|
||||
val timeout = Runnable {
|
||||
val resources = detachCurrentProvider(
|
||||
request = request,
|
||||
expectedCancellationSignal = cancellationSignal
|
||||
) ?: return@Runnable
|
||||
Log.w(TAG, "Location request timed out for ${attempt.provider}")
|
||||
releaseOneShotResources(resources)
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
|
||||
if (!attachCurrentProvider(request, cancellationSignal, null, timeout)) return
|
||||
handler.postDelayed(timeout, attempt.timeoutMs)
|
||||
|
||||
try {
|
||||
locationManager.getCurrentLocation(
|
||||
attempt.provider,
|
||||
cancellationSignal,
|
||||
context.mainExecutor
|
||||
) { location ->
|
||||
val resources = detachCurrentProvider(
|
||||
request = request,
|
||||
expectedCancellationSignal = cancellationSignal
|
||||
) ?: return@getCurrentLocation
|
||||
releaseOneShotResources(resources)
|
||||
|
||||
if (location != null && hasLocationPermission()) {
|
||||
finishOneShot(request, location)
|
||||
} else {
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val resources = detachCurrentProvider(
|
||||
request = request,
|
||||
expectedCancellationSignal = cancellationSignal
|
||||
)
|
||||
if (resources != null) {
|
||||
Log.w(TAG, "Unable to request ${attempt.provider} location", e)
|
||||
releaseOneShotResources(resources)
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun requestSingleLocationUpdate(
|
||||
request: PendingOneShot,
|
||||
attempt: ProviderAttempt
|
||||
) {
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
val resources = detachCurrentProvider(
|
||||
request = request,
|
||||
expectedListener = this
|
||||
) ?: return
|
||||
releaseOneShotResources(resources)
|
||||
|
||||
if (hasLocationPermission()) {
|
||||
finishOneShot(request, location)
|
||||
} else {
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerFound) {
|
||||
Log.w(TAG, "No location providers available for fresh location")
|
||||
callback(null)
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
|
||||
override fun onProviderEnabled(provider: String) = Unit
|
||||
override fun onProviderDisabled(provider: String) = Unit
|
||||
}
|
||||
val timeout = Runnable {
|
||||
val resources = detachCurrentProvider(
|
||||
request = request,
|
||||
expectedListener = listener
|
||||
) ?: return@Runnable
|
||||
Log.w(TAG, "Location request timed out for ${attempt.provider}")
|
||||
releaseOneShotResources(resources)
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
|
||||
if (!attachCurrentProvider(request, null, listener, timeout)) return
|
||||
handler.postDelayed(timeout, attempt.timeoutMs)
|
||||
|
||||
try {
|
||||
locationManager.requestSingleUpdate(attempt.provider, listener, null)
|
||||
if (!isCurrentProviderRequest(request, expectedListener = listener)) {
|
||||
runCatching { locationManager.removeUpdates(listener) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error requesting fresh location")
|
||||
callback(null)
|
||||
val resources = detachCurrentProvider(
|
||||
request = request,
|
||||
expectedListener = listener
|
||||
)
|
||||
if (resources != null) {
|
||||
Log.w(TAG, "Unable to request ${attempt.provider} location", e)
|
||||
releaseOneShotResources(resources)
|
||||
startNextOneShotProvider(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isProviderEnabled(provider: String): Boolean {
|
||||
return try {
|
||||
locationManager.isProviderEnabled(provider)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to inspect $provider provider", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachCurrentProvider(
|
||||
request: PendingOneShot,
|
||||
cancellationSignal: CancellationSignal?,
|
||||
listener: LocationListener?,
|
||||
timeoutRunnable: Runnable
|
||||
): Boolean = synchronized(activeOneShotRequests) {
|
||||
if (!isOneShotActiveLocked(request) || !hasLocationPermission()) {
|
||||
false
|
||||
} else {
|
||||
request.cancellationSignal = cancellationSignal
|
||||
request.listener = listener
|
||||
request.timeoutRunnable = timeoutRunnable
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCurrentProviderRequest(
|
||||
request: PendingOneShot,
|
||||
expectedCancellationSignal: CancellationSignal? = null,
|
||||
expectedListener: LocationListener? = null
|
||||
): Boolean = synchronized(activeOneShotRequests) {
|
||||
if (!isOneShotActiveLocked(request)) {
|
||||
false
|
||||
} else {
|
||||
(expectedCancellationSignal == null || request.cancellationSignal === expectedCancellationSignal) &&
|
||||
(expectedListener == null || request.listener === expectedListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun detachCurrentProvider(
|
||||
request: PendingOneShot,
|
||||
expectedCancellationSignal: CancellationSignal? = null,
|
||||
expectedListener: LocationListener? = null
|
||||
): OneShotResources? = synchronized(activeOneShotRequests) {
|
||||
if (!isCurrentProviderRequestLocked(request, expectedCancellationSignal, expectedListener)) {
|
||||
null
|
||||
} else {
|
||||
OneShotResources(
|
||||
cancellationSignal = request.cancellationSignal,
|
||||
listener = request.listener,
|
||||
timeoutRunnable = request.timeoutRunnable
|
||||
).also {
|
||||
request.cancellationSignal = null
|
||||
request.listener = null
|
||||
request.timeoutRunnable = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isOneShotActiveLocked(request: PendingOneShot): Boolean =
|
||||
!request.completed && activeOneShotRequests.contains(request)
|
||||
|
||||
private fun isCurrentProviderRequestLocked(
|
||||
request: PendingOneShot,
|
||||
expectedCancellationSignal: CancellationSignal?,
|
||||
expectedListener: LocationListener?
|
||||
): Boolean =
|
||||
isOneShotActiveLocked(request) &&
|
||||
(expectedCancellationSignal == null || request.cancellationSignal === expectedCancellationSignal) &&
|
||||
(expectedListener == null || request.listener === expectedListener)
|
||||
|
||||
private fun finishOneShot(request: PendingOneShot, location: Location?) {
|
||||
val resources = synchronized(activeOneShotRequests) {
|
||||
if (!isOneShotActiveLocked(request)) {
|
||||
null
|
||||
} else {
|
||||
request.completed = true
|
||||
activeOneShotRequests.remove(request)
|
||||
OneShotResources(
|
||||
cancellationSignal = request.cancellationSignal,
|
||||
listener = request.listener,
|
||||
timeoutRunnable = request.timeoutRunnable
|
||||
).also {
|
||||
request.cancellationSignal = null
|
||||
request.listener = null
|
||||
request.timeoutRunnable = null
|
||||
}
|
||||
}
|
||||
} ?: return
|
||||
|
||||
releaseOneShotResources(resources)
|
||||
request.callback(location.takeIf { hasLocationPermission() })
|
||||
}
|
||||
|
||||
private fun releaseOneShotResources(resources: OneShotResources) {
|
||||
resources.timeoutRunnable?.let(handler::removeCallbacks)
|
||||
resources.cancellationSignal?.cancel()
|
||||
resources.listener?.let { listener ->
|
||||
runCatching { locationManager.removeUpdates(listener) }
|
||||
.onFailure { Log.w(TAG, "Unable to remove one-shot location listener", it) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,26 +374,29 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
|
||||
) {
|
||||
if (!hasLocationPermission()) return
|
||||
|
||||
try {
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
if (LiveLocationPrivacyGate.isEnabled) callback(location)
|
||||
removeLocationUpdates(callback)
|
||||
|
||||
lateinit var listener: LocationListener
|
||||
listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
val isCurrentListener = synchronized(activeListeners) {
|
||||
activeListeners[callback] === listener
|
||||
}
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
override fun onProviderDisabled(provider: String) {}
|
||||
if (isCurrentListener && hasLocationPermission()) callback(location)
|
||||
}
|
||||
|
||||
// Store the listener so we can remove it later
|
||||
synchronized(activeListeners) {
|
||||
activeListeners[callback] = listener
|
||||
}
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
|
||||
override fun onProviderEnabled(provider: String) = Unit
|
||||
override fun onProviderDisabled(provider: String) = Unit
|
||||
}
|
||||
|
||||
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||
var registered = false
|
||||
|
||||
for (provider in providers) {
|
||||
if (locationManager.isProviderEnabled(provider)) {
|
||||
var registered = false
|
||||
var shouldCleanUp = false
|
||||
synchronized(activeListeners) {
|
||||
activeListeners[callback] = listener
|
||||
for (provider in listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)) {
|
||||
if (!isProviderEnabled(provider)) continue
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
provider,
|
||||
intervalMs,
|
||||
@ -200,62 +405,57 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
|
||||
)
|
||||
registered = true
|
||||
Log.d(TAG, "Registered updates for $provider")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to register updates for $provider", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!registered) {
|
||||
Log.w(TAG, "No providers enabled for continuous updates")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error requesting location updates")
|
||||
shouldCleanUp = !registered || activeListeners[callback] !== listener
|
||||
if (shouldCleanUp && activeListeners[callback] === listener) {
|
||||
activeListeners.remove(callback)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCleanUp) {
|
||||
runCatching { locationManager.removeUpdates(listener) }
|
||||
Log.w(TAG, "No system providers accepted continuous location updates")
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeLocationUpdates(callback: (Location) -> Unit) {
|
||||
try {
|
||||
val listener = synchronized(activeListeners) {
|
||||
activeListeners.remove(callback)
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
locationManager.removeUpdates(listener)
|
||||
Log.d(TAG, "Removed location updates")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error removing updates")
|
||||
val listener = synchronized(activeListeners) {
|
||||
activeListeners.remove(callback)
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
runCatching { locationManager.removeUpdates(listener) }
|
||||
.onFailure { Log.w(TAG, "Unable to remove location updates", it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
try {
|
||||
// Cancel continuous updates
|
||||
synchronized(activeListeners) {
|
||||
for ((_, listener) in activeListeners) {
|
||||
try { locationManager.removeUpdates(listener) } catch (_: Exception) {}
|
||||
}
|
||||
activeListeners.clear()
|
||||
}
|
||||
|
||||
// Cancel one-shot requests
|
||||
synchronized(activeOneShotListeners) {
|
||||
for ((_, listener) in activeOneShotListeners) {
|
||||
try { locationManager.removeUpdates(listener) } catch (_: Exception) {}
|
||||
}
|
||||
activeOneShotListeners.clear()
|
||||
|
||||
for ((_, runnable) in activeOneShotRunnables) {
|
||||
handler.removeCallbacks(runnable)
|
||||
}
|
||||
activeOneShotRunnables.clear()
|
||||
}
|
||||
synchronized(activeOneShotCancellationSignals) {
|
||||
activeOneShotCancellationSignals.values.forEach { it.cancel() }
|
||||
activeOneShotCancellationSignals.clear()
|
||||
}
|
||||
Log.d(TAG, "Cancelled all system location requests")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error cancelling system provider")
|
||||
val listeners = synchronized(activeListeners) {
|
||||
activeListeners.values.toList().also { activeListeners.clear() }
|
||||
}
|
||||
listeners.forEach { listener ->
|
||||
runCatching { locationManager.removeUpdates(listener) }
|
||||
.onFailure { Log.w(TAG, "Unable to remove location updates", it) }
|
||||
}
|
||||
|
||||
val oneShotResources = synchronized(activeOneShotRequests) {
|
||||
activeOneShotRequests.map { request ->
|
||||
request.completed = true
|
||||
OneShotResources(
|
||||
cancellationSignal = request.cancellationSignal,
|
||||
listener = request.listener,
|
||||
timeoutRunnable = request.timeoutRunnable
|
||||
)
|
||||
}.also {
|
||||
activeOneShotRequests.clear()
|
||||
}
|
||||
}
|
||||
oneShotResources.forEach(::releaseOneShotResources)
|
||||
Log.d(TAG, "Cancelled all system location requests")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +1,30 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatBottomSheet
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
|
||||
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
|
||||
@ -33,6 +47,7 @@ fun LocationNotesSheetPresenter(
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val systemLocationEnabled by locationManager.systemLocationEnabled.collectAsStateWithLifecycle()
|
||||
val isLoadingLocation by locationManager.isLoadingLocation.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
|
||||
@ -57,7 +72,8 @@ fun LocationNotesSheetPresenter(
|
||||
// No building geohash available - show error state (matches iOS)
|
||||
LocationNotesErrorSheet(
|
||||
onDismiss = onDismiss,
|
||||
locationManager = locationManager
|
||||
locationManager = locationManager,
|
||||
systemLocationEnabled = systemLocationEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -106,8 +122,41 @@ private fun LocationNotesAcquiringSheet(
|
||||
@Composable
|
||||
private fun LocationNotesErrorSheet(
|
||||
onDismiss: () -> Unit,
|
||||
locationManager: LocationChannelManager
|
||||
locationManager: LocationChannelManager,
|
||||
systemLocationEnabled: Boolean
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var locationPermissionRequestAttempted by rememberSaveable { mutableStateOf(false) }
|
||||
var awaitingLocationSettingsRecovery by rememberSaveable { mutableStateOf(false) }
|
||||
val locationPermissionLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissionResults ->
|
||||
val locationGranted =
|
||||
permissionResults[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
|
||||
permissionResults[Manifest.permission.ACCESS_COARSE_LOCATION] == true
|
||||
val permissionState = locationManager.syncPermissionState()
|
||||
|
||||
if (locationGranted && permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
||||
locationManager.enableLocationServices()
|
||||
locationManager.enableLocationChannels()
|
||||
locationManager.refreshChannels()
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(awaitingLocationSettingsRecovery, systemLocationEnabled) {
|
||||
if (awaitingLocationSettingsRecovery &&
|
||||
systemLocationEnabled &&
|
||||
locationManager.syncPermissionState() == LocationChannelManager.PermissionState.AUTHORIZED
|
||||
) {
|
||||
awaitingLocationSettingsRecovery = false
|
||||
locationManager.enableLocationServices()
|
||||
locationManager.enableLocationChannels()
|
||||
locationManager.refreshChannels()
|
||||
}
|
||||
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
BitchatBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
@ -127,19 +176,41 @@ private fun LocationNotesErrorSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Location permission is required for notes",
|
||||
text = stringResource(R.string.location_notes_location_unavailable),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = {
|
||||
// UNIFIED FIX: Enable location services first (user toggle)
|
||||
locationManager.enableLocationServices()
|
||||
// Then request location channels (which will also request permission if needed)
|
||||
locationManager.enableLocationChannels()
|
||||
locationManager.refreshChannels()
|
||||
when {
|
||||
!systemLocationEnabled -> {
|
||||
awaitingLocationSettingsRecovery = runCatching {
|
||||
context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
|
||||
}.isSuccess
|
||||
}
|
||||
locationManager.syncPermissionState() != LocationChannelManager.PermissionState.AUTHORIZED -> {
|
||||
if (locationPermissionRequestAttempted &&
|
||||
hasPermanentlyDeniedLocationPermission(context)
|
||||
) {
|
||||
awaitingLocationSettingsRecovery = openAppLocationSettings(context)
|
||||
} else {
|
||||
locationPermissionRequestAttempted = true
|
||||
locationPermissionLauncher.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
locationManager.enableLocationServices()
|
||||
locationManager.enableLocationChannels()
|
||||
locationManager.refreshChannels()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text("Enable Location")
|
||||
Text(stringResource(R.string.enable_location_services))
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,3 +226,30 @@ private fun LocationNotesErrorSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasPermanentlyDeniedLocationPermission(context: Context): Boolean {
|
||||
val activity = context.findActivity() ?: return false
|
||||
val locationPermissions = listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
return locationPermissions.all { permission ->
|
||||
ActivityCompat.checkSelfPermission(context, permission) != android.content.pm.PackageManager.PERMISSION_GRANTED &&
|
||||
!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAppLocationSettings(context: Context): Boolean {
|
||||
return runCatching {
|
||||
context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", context.packageName, null)
|
||||
})
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
private tailrec fun Context.findActivity(): Activity? = when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
@ -442,6 +442,7 @@
|
||||
<string name="location_notes_relays_unavailable">Geo relays unavailable; notes paused</string>
|
||||
<string name="location_notes_no_relays_title">No geo relays nearby</string>
|
||||
<string name="location_notes_no_relays_desc">Notes rely on geo relays. Check connection and try again.</string>
|
||||
<string name="location_notes_location_unavailable">Unable to determine your current location. Check device location and try again.</string>
|
||||
<string name="loading_location_notes">Loading notes…</string>
|
||||
<string name="location_notes_empty_title">No notes yet</string>
|
||||
<string name="location_notes_empty_desc">Be the first to add one for this spot.</string>
|
||||
@ -655,3 +656,4 @@
|
||||
<string name="about_system_default">System default</string>
|
||||
<string name="about_select_language">Select language</string>
|
||||
</resources>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user