mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-10-25 07:40:36 +00:00
Merge dcee3b417b07943ab27fd4efe4f22307064297dd into 4f828b5643ff87a5064c8d384d0a4cb64001d2b2
This commit is contained in:
commit
de1e318a93
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user