Merge dcee3b417b07943ab27fd4efe4f22307064297dd into 4f828b5643ff87a5064c8d384d0a4cb64001d2b2

This commit is contained in:
liuyunming521520-cell 2026-08-26 12:42:00 -04:00 committed by GitHub
commit de1e318a93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 156 additions and 70 deletions

View File

@ -18,7 +18,8 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
}
private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
private val systemFallback = SystemLocationProvider(context)
// Map to keep track of callbacks to remove them later
private val activeCallbacks = mutableMapOf<(Location) -> Unit, LocationCallback>()
private val activeCurrentLocationRequests = mutableSetOf<CancellationTokenSource>()
@ -40,15 +41,19 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
try {
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
if (location != null && LiveLocationPrivacyGate.isEnabled) {
callback(location)
} else {
systemFallback.getLastKnownLocation(callback)
}
}
.addOnFailureListener { e ->
.addOnFailureListener {
Log.e(TAG, "Error getting last-known fused location")
callback(null)
systemFallback.getLastKnownLocation(callback)
}
} catch (e: Exception) {
} catch (_: Exception) {
Log.e(TAG, "Exception getting last-known fused location")
callback(null)
systemFallback.getLastKnownLocation(callback)
}
}
@ -72,20 +77,24 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
fusedLocationClient.getCurrentLocation(request, cancellation.token)
.addOnSuccessListener { location ->
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
if (location != null && LiveLocationPrivacyGate.isEnabled) {
callback(location)
} else {
systemFallback.requestFreshLocation(callback)
}
}
.addOnFailureListener { e ->
.addOnFailureListener {
Log.e(TAG, "Error getting fresh fused location")
callback(null)
systemFallback.requestFreshLocation(callback)
}
.addOnCompleteListener {
synchronized(activeCurrentLocationRequests) {
activeCurrentLocationRequests.remove(cancellation)
}
}
} catch (e: Exception) {
} catch (_: Exception) {
Log.e(TAG, "Exception getting fresh fused location")
callback(null)
systemFallback.requestFreshLocation(callback)
}
}
@ -119,27 +128,40 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
request,
locationCallback,
Looper.getMainLooper()
)
Log.d(TAG, "Registered fused updates")
).addOnSuccessListener {
Log.d(TAG, "Registered fused updates")
}.addOnFailureListener {
val shouldStartFallback = synchronized(activeCallbacks) {
activeCallbacks[callback] === locationCallback
}
if (shouldStartFallback) {
Log.w(TAG, "Fused updates unavailable; using system location provider")
systemFallback.requestLocationUpdates(intervalMs, minDistanceMeters, callback)
}
}
} catch (e: Exception) {
} catch (_: Exception) {
Log.e(TAG, "Error requesting fused updates")
synchronized(activeCallbacks) {
activeCallbacks.remove(callback)
}
systemFallback.requestLocationUpdates(intervalMs, minDistanceMeters, callback)
}
}
override fun removeLocationUpdates(callback: (Location) -> Unit) {
val locationCallback = synchronized(activeCallbacks) {
activeCallbacks.remove(callback)
}
try {
val locationCallback = synchronized(activeCallbacks) {
activeCallbacks.remove(callback)
}
if (locationCallback != null) {
fusedLocationClient.removeLocationUpdates(locationCallback)
Log.d(TAG, "Removed fused updates")
}
} catch (e: Exception) {
} catch (_: Exception) {
Log.e(TAG, "Error removing fused updates")
}
systemFallback.removeLocationUpdates(callback)
}
override fun cancel() {
@ -155,8 +177,9 @@ internal class FusedLocationProvider(private val context: Context) : LocationPro
activeCurrentLocationRequests.clear()
}
Log.d(TAG, "Cancelled all fused updates")
} catch (e: Exception) {
} catch (_: Exception) {
Log.e(TAG, "Error cancelling fused provider")
}
systemFallback.cancel()
}
}

View File

@ -127,7 +127,7 @@ 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
)

View File

@ -14,7 +14,6 @@ import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.viewfinder.core.ImplementationMode
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -87,6 +86,7 @@ import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicReference
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -321,6 +321,7 @@ private fun ScanTabContent(
onScan: (String) -> Unit
) {
val permissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
var cameraUnavailable by remember { mutableStateOf(false) }
Column(
modifier = Modifier
@ -338,31 +339,52 @@ private fun ScanTabContent(
.background(Color.Black),
contentAlignment = Alignment.Center
) {
ScannerView(onScan = onScan)
// Overlay border
Box(
modifier = Modifier
.size(280.dp)
.border(2.dp, accent.copy(alpha = 0.8f), RoundedCornerShape(16.dp))
)
// Corner accents for the overlay
Box(modifier = Modifier.size(260.dp)) {
// This could be drawn with Canvas for cooler effect, but simple border is cleaner for now
if (cameraUnavailable) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = stringResource(R.string.verify_camera_unavailable),
color = Color.White,
fontFamily = BitchatFontFamily,
textAlign = TextAlign.Center
)
Button(
onClick = { cameraUnavailable = false },
colors = ButtonDefaults.buttonColors(containerColor = accent)
) {
Text(
text = stringResource(R.string.verify_retry_camera),
fontFamily = BitchatFontFamily
)
}
}
} else {
ScannerView(
onScan = onScan,
onCameraUnavailable = { cameraUnavailable = true }
)
Box(
modifier = Modifier
.size(280.dp)
.border(2.dp, accent.copy(alpha = 0.8f), RoundedCornerShape(16.dp))
)
Text(
text = stringResource(R.string.verify_scan_prompt_friend),
color = Color.White,
fontFamily = BitchatFontFamily,
fontSize = 12.sp,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
.background(Color.Black.copy(alpha = 0.6f), RoundedCornerShape(8.dp))
.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
Text(
text = stringResource(R.string.verify_scan_prompt_friend),
color = Color.White,
fontFamily = BitchatFontFamily,
fontSize = 12.sp,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
.background(Color.Black.copy(alpha = 0.6f), RoundedCornerShape(8.dp))
.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
} else {
Column(
@ -407,70 +429,104 @@ private fun ScanTabContent(
@Composable
private fun ScannerView(
onScan: (String) -> Unit
onScan: (String) -> Unit,
onCameraUnavailable: () -> Unit
) {
val context = LocalContext.current
val context = LocalContext.current.applicationContext
val lifecycleOwner = LocalLifecycleOwner.current
var lastValid by remember { mutableStateOf<String?>(null) }
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
val cameraProviderFuture = remember(context) { ProcessCameraProvider.getInstance(context) }
val cameraExecutor: ExecutorService = remember(lifecycleOwner) { Executors.newSingleThreadExecutor() }
val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
val surfaceRequest by surfaceRequests.collectAsState(initial = null)
val mainHandler = remember { Handler(Looper.getMainLooper()) }
val activeSession = remember { AtomicReference<Any?>(null) }
val onCodeState = rememberUpdatedState(onScan)
val analyzer = remember {
val onCameraUnavailableState = rememberUpdatedState(onCameraUnavailable)
val analyzer = remember(lifecycleOwner) {
QRCodeAnalyzer { text ->
val session = activeSession.get() ?: return@QRCodeAnalyzer
mainHandler.post {
if (text == lastValid) return@post
if (activeSession.get() !== session || text == lastValid) return@post
lastValid = text
onCodeState.value(text)
}
}
}
DisposableEffect(Unit) {
DisposableEffect(cameraProviderFuture, lifecycleOwner, cameraExecutor, analyzer) {
val executor = ContextCompat.getMainExecutor(context)
var cameraProvider: ProcessCameraProvider? = null
var preview: Preview? = null
var analysis: ImageAnalysis? = null
val session = Any()
activeSession.set(session)
fun isCurrentSession() = activeSession.get() === session
cameraProviderFuture.addListener(
{
val provider = cameraProviderFuture.get()
cameraProvider = provider
val preview = Preview.Builder().build().also {
it.setSurfaceProvider { request -> surfaceRequests.value = request }
}
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(cameraExecutor, analyzer) }
if (!isCurrentSession()) return@addListener
runCatching {
provider.unbindAll()
try {
val provider = cameraProviderFuture.get()
if (!isCurrentSession() || !provider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)) {
if (isCurrentSession()) onCameraUnavailableState.value()
return@addListener
}
val scannerPreview = Preview.Builder().build().also {
it.setSurfaceProvider { request ->
if (isCurrentSession()) {
surfaceRequests.value = request
} else {
request.willNotProvideSurface()
}
}
}
val scannerAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(cameraExecutor, analyzer) }
cameraProvider = provider
preview = scannerPreview
analysis = scannerAnalysis
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis
scannerPreview,
scannerAnalysis
)
}.onFailure {
Log.w("VerificationSheet", "Failed to bind camera: ${it.message}")
} catch (exception: Exception) {
if (isCurrentSession()) {
Log.e("VerificationSheet", "Unable to start QR camera", exception)
onCameraUnavailableState.value()
}
}
},
executor
)
onDispose {
activeSession.compareAndSet(session, null)
surfaceRequests.value = null
runCatching { cameraProvider?.unbindAll() }
analysis?.clearAnalyzer()
val provider = cameraProvider
val scannerPreview = preview
val scannerAnalysis = analysis
if (provider != null && scannerPreview != null && scannerAnalysis != null) {
runCatching { provider.unbind(scannerPreview, scannerAnalysis) }
}
cameraExecutor.shutdown()
analyzer.close()
}
}
surfaceRequest?.let { request ->
CameraXViewfinder(
surfaceRequest = request,
implementationMode = ImplementationMode.EMBEDDED,
modifier = Modifier.fillMaxSize()
)
}
@ -521,6 +577,10 @@ private class QRCodeAnalyzer(
.build()
)
fun close() {
scanner.close()
}
@ExperimentalGetImage
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: run {

View File

@ -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>
@ -483,6 +484,8 @@
<string name="verify_qr_unavailable">QR unavailable</string>
<string name="verify_camera_permission">Camera permission is needed to scan QR codes</string>
<string name="verify_request_camera">Enable camera</string>
<string name="verify_camera_unavailable">Camera is unavailable. Please try again.</string>
<string name="verify_retry_camera">Retry camera</string>
<string name="verify_paste_label">Paste verification URL</string>
<string name="verify_validate">Validate</string>
<string name="verify_scanned">Verification requested</string>