feat: add airtest scripts for camera and gallery testing

This commit is contained in:
2026-03-01 23:17:55 +08:00
parent 3ee14eabe6
commit a7c6e6b909
10 changed files with 898 additions and 17 deletions

View File

@@ -76,6 +76,7 @@ import com.inspection.camera.util.PermissionManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.util.concurrent.Executors
@@ -158,17 +159,17 @@ fun CameraScreen(
}
}
// 获取位置
// 获取位置10秒超时
LaunchedEffect(permissionsState.allPermissionsGranted, hasLocationPermission) {
isLocationLoading = true
if (permissionsState.allPermissionsGranted && hasLocationPermission) {
try {
Log.d("CameraScreen", "Getting location...")
locationText = locationHelper.getLocationInfo()
Log.d("CameraScreen", "Location result: $locationText")
if (locationText.isEmpty()) {
locationText = "定位失败"
Log.d("CameraScreen", "Getting location with 10s timeout...")
val result = withTimeoutOrNull(10000) {
locationHelper.getLocationInfo()
}
locationText = result ?: "定位失败"
Log.d("CameraScreen", "Location result: $locationText")
} catch (e: Exception) {
Log.e("CameraScreen", "Location error", e)
locationText = "定位失败"
@@ -200,6 +201,9 @@ fun CameraScreen(
onComplete = { uri ->
capturedImages.add(uri)
isCapturing = false
},
onError = {
isCapturing = false
}
)
}
@@ -299,7 +303,7 @@ private fun CameraContent(
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
@@ -307,8 +311,10 @@ private fun CameraContent(
preview,
imageCapture
)
Log.d("CameraScreen", "Camera initialized successfully")
} catch (e: Exception) {
Log.e("CameraScreen", "Camera binding failed", e)
imageCapture = null
}
}, ContextCompat.getMainExecutor(context))
@@ -462,24 +468,31 @@ private suspend fun getValidLocationTextForPhoto(
manualAddress: String,
locationHelper: LocationHelper
): String {
Log.d("CameraScreen", "getValidLocationTextForPhoto called, currentLocationText=$currentLocationText")
// 检查当前定位文本是否有效
val invalidTexts = listOf("正在定位...", "定位失败", "请授予定位权限")
val isInvalid = currentLocationText.isBlank() || invalidTexts.contains(currentLocationText)
if (!isInvalid) {
Log.d("CameraScreen", "Using current location text: $currentLocationText")
return currentLocationText
}
// 使用手动地址
if (manualAddress.isNotBlank()) {
Log.d("CameraScreen", "Using manual address: $manualAddress")
return manualAddress
}
// 尝试快速获取当前位置(使用缓存)
Log.d("CameraScreen", "Requesting new location...")
val location = locationHelper.getCurrentLocation()
return location?.let {
val result = location?.let {
"${"%.4f".format(it.latitude)}, ${"%.4f".format(it.longitude)}"
} ?: "未知地点"
Log.d("CameraScreen", "Got location result: $result")
return result
}
private fun capturePhoto(
@@ -489,8 +502,10 @@ private fun capturePhoto(
watermarkStyle: WatermarkStyle,
imageQuality: ImageQuality,
locationText: String,
onComplete: (Uri) -> Unit
onComplete: (Uri) -> Unit,
onError: () -> Unit = {}
) {
Log.d("CameraScreen", "capturePhoto called, locationText=$locationText")
val photoFile = File(
context.cacheDir,
"photo_${System.currentTimeMillis()}.jpg"
@@ -504,9 +519,17 @@ private fun capturePhoto(
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
Log.d("CameraScreen", "Photo saved, adding watermark with locationText=$locationText")
val bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
if (bitmap != null) {
if (bitmap == null) {
Log.e("CameraScreen", "Failed to decode photo file: ${photoFile.absolutePath}")
onError()
return
}
try {
val timeText = ImageProcessor.getCurrentTimeText()
Log.d("CameraScreen", "Adding watermark: timeText=$timeText, locationText=$locationText")
val watermarkedBitmap = ImageProcessor.addWatermark(
bitmap,
timeText,
@@ -521,12 +544,22 @@ private fun capturePhoto(
bitmap.recycle()
watermarkedBitmap.recycle()
uri?.let { onComplete(it) }
if (uri != null) {
onComplete(uri)
} else {
Log.e("CameraScreen", "Failed to save image to gallery")
onError()
}
} catch (e: Exception) {
Log.e("CameraScreen", "Error processing image", e)
bitmap.recycle()
onError()
}
}
override fun onError(exception: ImageCaptureException) {
Log.e("CameraScreen", "Photo capture failed", exception)
onError()
}
}
)

View File

@@ -69,6 +69,7 @@ fun GalleryScreen(
var selectedImages by remember { mutableStateOf<Set<Uri>>(emptySet()) }
var showDeleteDialog by remember { mutableStateOf(false) }
var isSelectionMode by remember { mutableStateOf(false) }
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
// 加载图片
LaunchedEffect(Unit) {
@@ -149,6 +150,9 @@ fun GalleryScreen(
} else {
selectedImages + uri
}
} else {
// 非选择模式下,点击打开大图查看器
selectedImageUri = uri
}
},
onLongClick = {
@@ -189,6 +193,32 @@ fun GalleryScreen(
}
)
}
// 大图查看对话框
if (selectedImageUri != null) {
AlertDialog(
onDismissRequest = { selectedImageUri = null },
title = { Text("图片预览") },
text = {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(selectedImageUri)
.crossfade(true)
.build(),
contentDescription = "大图预览",
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
},
confirmButton = {
TextButton(onClick = { selectedImageUri = null }) {
Text("关闭")
}
}
)
}
}
@Composable

View File

@@ -33,6 +33,44 @@ object ImageProcessor {
private val dateFormat = SimpleDateFormat("yyyy年-MM月-dd日 HH:mm:ss", Locale.getDefault())
private val fileNameFormat = SimpleDateFormat("yyyyMMddHHmm", Locale.getDefault())
/**
* 尝试加载图片 Bitmap
*/
private fun tryLoadBitmap(context: Context, imageItem: ImageItem): Bitmap? {
android.util.Log.d("ImageProcessor", "Loading image, URI: ${imageItem.uri}, path: ${imageItem.path}")
// 优先尝试 URI 加载
try {
val uri = imageItem.uri
android.util.Log.d("ImageProcessor", "Trying to open URI: $uri")
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
android.util.Log.d("ImageProcessor", "URI loaded bitmap: ${bitmap != null}")
return bitmap
}
} catch (e: Exception) {
android.util.Log.e("ImageProcessor", "URI load failed: ${e.message}")
}
// 尝试从文件路径加载
if (imageItem.path.isNotEmpty()) {
try {
val file = java.io.File(imageItem.path)
android.util.Log.d("ImageProcessor", "Trying file: ${file.absolutePath}, exists: ${file.exists()}")
if (file.exists()) {
val bitmap = BitmapFactory.decodeFile(imageItem.path)
android.util.Log.d("ImageProcessor", "File loaded bitmap: ${bitmap != null}")
return bitmap
}
} catch (e: Exception) {
android.util.Log.e("ImageProcessor", "File load failed: ${e.message}")
}
}
android.util.Log.w("ImageProcessor", "Failed to load image")
return null
}
/**
* 获取当前时间戳文本
*/
@@ -101,9 +139,10 @@ object ImageProcessor {
}
/**
* 合成多张图片
* 合成多张图片(支持 URI
*/
fun mergeImages(
context: Context,
images: List<ImageItem>,
layoutType: MergeLayoutType,
quality: ImageQuality
@@ -114,7 +153,6 @@ object ImageProcessor {
val cols = layoutType.cols
val rows = layoutType.rows
val imageCount = images.size.coerceAtMost(rows * cols)
val outputWidth = 1920
val outputHeight = 1080
@@ -137,10 +175,10 @@ object ImageProcessor {
val top = row * cellHeight
try {
val sourceBitmap = BitmapFactory.decodeFile(imageItem.path)
?: return@forEachIndexed
val sourceBitmap = tryLoadBitmap(context, imageItem)
sourceBitmap ?: return@forEachIndexed
// 缩放并居中裁剪
val scaledBitmap = scaleAndCropBitmap(sourceBitmap, cellWidth, cellHeight)
val dstRect = Rect(left, top, left + cellWidth, top + cellHeight)
canvas.drawBitmap(scaledBitmap, null, dstRect, paint)
@@ -150,7 +188,6 @@ object ImageProcessor {
}
sourceBitmap.recycle()
} catch (e: Exception) {
// 加载失败绘制占位
val placeholderPaint = Paint().apply {
color = Color.LTGRAY
}