Add SimpleCameraActivity for crash debugging - minimal camera with only preview and capture

This commit is contained in:
xiajiid
2026-02-08 22:08:39 +08:00
parent 5eceac79ed
commit b261fdb4b3
7 changed files with 342 additions and 26 deletions

View File

@@ -26,6 +26,10 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".SimpleCameraActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application> </application>
</manifest> </manifest>

View File

@@ -49,6 +49,7 @@ class CameraActivity : AppCompatActivity() {
private lateinit var photoPreviewLayout: LinearLayout private lateinit var photoPreviewLayout: LinearLayout
private var imageCapture: ImageCapture? = null private var imageCapture: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService private lateinit var cameraExecutor: ExecutorService
private var cameraProvider: ProcessCameraProvider? = null
// 存储拍摄的图片URI // 存储拍摄的图片URI
private val capturedImageUris = mutableListOf<Uri>() private val capturedImageUris = mutableListOf<Uri>()
@@ -65,6 +66,9 @@ class CameraActivity : AppCompatActivity() {
settingsButton = findViewById(R.id.settingsButton) settingsButton = findViewById(R.id.settingsButton)
photoPreviewLayout = findViewById(R.id.photoPreviewLayout) photoPreviewLayout = findViewById(R.id.photoPreviewLayout)
// 初始化线程池
cameraExecutor = Executors.newSingleThreadExecutor()
// 请求权限 // 请求权限
if (allPermissionsGranted()) { if (allPermissionsGranted()) {
startCamera() startCamera()
@@ -77,9 +81,12 @@ class CameraActivity : AppCompatActivity() {
// 设置点击监听器 // 设置点击监听器
captureButton.setOnClickListener { takePhoto() } captureButton.setOnClickListener { takePhoto() }
settingsButton.setOnClickListener { openSettings() } settingsButton.setOnClickListener { openSettings() }
}
// 初始化线程池 override fun onDestroy() {
cameraExecutor = Executors.newSingleThreadExecutor() super.onDestroy()
cameraExecutor.shutdown()
cameraProvider?.unbindAll()
} }
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
@@ -90,35 +97,39 @@ class CameraActivity : AppCompatActivity() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this) val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({ cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try { try {
cameraProvider.unbindAll() cameraProvider = cameraProviderFuture.get()
cameraProvider.bindToLifecycle(
// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraProvider?.unbindAll()
cameraProvider?.bindToLifecycle(
this, cameraSelector, preview, imageCapture this, cameraSelector, preview, imageCapture
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "相机绑定失败", e) Log.e(TAG, "相机初始化失败: ${e.message}", e)
Toast.makeText(this, "相机启动失败: ${e.message}", Toast.LENGTH_LONG).show()
} }
}, ContextCompat.getMainExecutor(this)) }, ContextCompat.getMainExecutor(this))
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun takePhoto() { private fun takePhoto() {
val imageCapture = this.imageCapture ?: return val imageCapture = this.imageCapture ?: run {
Toast.makeText(this, "相机未准备好", Toast.LENGTH_SHORT).show()
return
}
val outputOptions = createOutputFileOptions() val outputOptions = createOutputFileOptions()
@@ -143,7 +154,7 @@ class CameraActivity : AppCompatActivity() {
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "拍照失败: ${exception.message}", exception) Log.e(TAG, "拍照失败: ${exception.message}", exception)
Toast.makeText(baseContext, "拍照失败", Toast.LENGTH_SHORT).show() Toast.makeText(baseContext, "拍照失败: ${exception.message}", Toast.LENGTH_LONG).show()
} }
} }
) )
@@ -308,8 +319,9 @@ class CameraActivity : AppCompatActivity() {
if (allPermissionsGranted()) { if (allPermissionsGranted()) {
startCamera() startCamera()
} else { } else {
Toast.makeText(this, "权限被拒绝,相机功能不可用", Toast.LENGTH_SHORT).show() Toast.makeText(this, "相机权限被拒绝,应用无法使用相机功能", Toast.LENGTH_LONG).show()
// 可以在这里提供降级功能,比如手动输入位置信息 // 可以选择关闭应用或提供替代功能
finish()
} }
} }
} }

View File

@@ -9,8 +9,8 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// 直接启动相机Activity // 启动极简相机Activity
startActivity(Intent(this, CameraActivity::class.java)) startActivity(Intent(this, SimpleCameraActivity::class.java))
finish() // 关闭MainActivity避免返回键回到空白页面 finish() // 关闭MainActivity避免返回键回到空白页面
} }
} }

View File

@@ -0,0 +1,170 @@
package com.example.app
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class SimpleCameraActivity : AppCompatActivity() {
companion object {
private const val TAG = "SimpleCamera"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA
)
}
private lateinit var viewFinder: PreviewView
private lateinit var captureButton: FloatingActionButton
private var imageCapture: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService
private var cameraProvider: ProcessCameraProvider? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_camera)
// 初始化视图
viewFinder = findViewById(R.id.viewFinder)
captureButton = findViewById(R.id.captureButton)
// 初始化线程池
cameraExecutor = Executors.newSingleThreadExecutor()
// 请求权限
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
// 设置拍照按钮点击监听器
captureButton.setOnClickListener { takePhoto() }
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
cameraProvider?.unbindAll()
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
try {
cameraProvider = cameraProviderFuture.get()
// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
cameraProvider?.unbindAll()
cameraProvider?.bindToLifecycle(
this, cameraSelector, preview, imageCapture
)
Log.d(TAG, "相机启动成功")
} catch (e: Exception) {
Log.e(TAG, "相机初始化失败: ${e.message}", e)
Toast.makeText(this, "相机启动失败: ${e.message}", Toast.LENGTH_LONG).show()
finish()
}
}, ContextCompat.getMainExecutor(this))
}
private fun takePhoto() {
val imageCapture = this.imageCapture ?: run {
Toast.makeText(this, "相机未准备好", Toast.LENGTH_SHORT).show()
return
}
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "巡检_$timestamp")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/LogCam")
}
}
val contentResolver = contentResolver
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (uri == null) {
Toast.makeText(this, "创建文件失败", Toast.LENGTH_SHORT).show()
return
}
val outputOptions = ImageCapture.OutputFileOptions.Builder(contentResolver, uri, contentValues).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = outputFileResults.savedUri ?: return
val msg = "照片已保存: ${savedUri.lastPathSegment}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "拍照失败: ${exception.message}", exception)
Toast.makeText(baseContext, "拍照失败: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this, "相机权限被拒绝,应用无法使用相机功能", Toast.LENGTH_LONG).show()
finish()
}
}
}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SimpleCameraActivity">
<!-- 相机预览视图 -->
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 拍照按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/captureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:src="@android:drawable/ic_menu_camera"
app:backgroundTint="@color/primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- 状态提示 -->
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="极简相机 - 点击下方按钮拍照"
android:textColor="@android:color/white"
android:textSize="14sp"
android:background="#80000000"
android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -7,4 +7,5 @@
<color name="teal_700">#FF018786</color> <color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="primary">#FF6200EE</color>
</resources> </resources>

83
create-signing.sh Normal file
View File

@@ -0,0 +1,83 @@
#!/bin/bash
# 创建Android应用签名文件并生成Base64字符串
echo "=== Android应用签名生成工具 ==="
echo ""
# 检查keytool是否可用
if ! command -v keytool &> /dev/null; then
echo "❌ keytool命令未找到请安装Java JDK"
exit 1
fi
# 签名配置
KEYSTORE_NAME="logcam-release.jks"
ALIAS="logcam-release-key"
STORE_PASS="android123"
KEY_PASS="android123"
DNAME="CN=LogCam, OU=Development, O=Example, L=Beijing, ST=Beijing, C=CN"
echo "📋 签名配置:"
echo " - 密钥库文件: $KEYSTORE_NAME"
echo " - 别名: $ALIAS"
echo " - 密钥库密码: $STORE_PASS"
echo " - 密钥密码: $KEY_PASS"
echo ""
# 生成签名文件
echo "🔑 生成签名文件..."
keytool -genkeypair -v \
-keystore "$KEYSTORE_NAME" \
-keyalg RSA -keysize 2048 \
-validity 10000 \
-alias "$ALIAS" \
-storepass "$STORE_PASS" \
-keypass "$KEY_PASS" \
-dname "$DNAME"
if [ $? -eq 0 ]; then
echo "✅ 签名文件生成成功: $KEYSTORE_NAME"
ls -la "$KEYSTORE_NAME"
else
echo "❌ 签名文件生成失败"
exit 1
fi
echo ""
echo "🔄 生成Base64字符串..."
# 生成Base64字符串
SIGNING_KEY_BASE64=$(base64 -w 0 "$KEYSTORE_NAME")
if [ $? -eq 0 ]; then
echo "✅ Base64字符串生成成功"
else
echo "❌ Base64转换失败"
exit 1
fi
echo ""
echo "📋 GitHub Secrets配置信息:"
echo "========================================"
echo "SIGNING_KEY (密钥文件Base64):"
echo "$SIGNING_KEY_BASE64"
echo "========================================"
echo ""
echo "ALIAS (别名): $ALIAS"
echo "KEYSTORE_PASSWORD (密钥库密码): $STORE_PASS"
echo "KEY_PASSWORD (密钥密码): $KEY_PASS"
echo "========================================"
echo ""
echo "📱 后续步骤:"
echo "1. 访问 https://github.com/xiajid/logcam/settings/secrets/actions"
echo "2. 创建以下Secrets:"
echo " - SIGNING_KEY: 粘贴上面的Base64字符串"
echo " - ALIAS: \"$ALIAS\""
echo " - KEYSTORE_PASSWORD: \"$STORE_PASS\""
echo " - KEY_PASSWORD: \"$KEY_PASS\""
echo "3. 提交新工作流文件"
echo "4. 触发构建获得签名版APK"
echo ""
echo "🔧 签名文件信息:"
keytool -list -v -keystore "$KEYSTORE_NAME" -storepass "$STORE_PASS" | grep -E "别名|有效期|指纹"
echo ""
echo "✅ 签名流程完成"