AssimilateTranslate/app/src/main/java/com/assimilate/alltrans/viewui/PhotoImageActivity.kt
2024-08-02 18:46:46 +08:00

501 lines
19 KiB
Kotlin

package com.assimilate.alltrans.viewui
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.util.Pair
import android.view.View
import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
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 androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.assimilate.alltrans.R
import com.assimilate.alltrans.adapters.LanguageAdapter
import com.assimilate.alltrans.common.BitmapUtils
import com.assimilate.alltrans.keepmodel.Language
import com.assimilate.alltrans.keepmodel.LanguagesConstants
import com.assimilate.alltrans.keepmodel.PreferenceLanguageUtils
import com.assimilate.alltrans.common.TextRecognitionProcessor
import com.assimilate.alltrans.common.VisionImageProcessor
import com.assimilate.alltrans.common.Widget
import com.assimilate.alltrans.databinding.ActivityStillImageBinding
import com.google.android.gms.common.annotation.KeepName
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.common.util.concurrent.ListenableFuture
import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions
import com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions
import com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions
import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
@KeepName
class PhotoImageActivity : AppCompatActivity() {
private var selectedMode = TEXT_RECOGNITION_CHINESE
private var selectedSize: String? = SIZE_SCREEN
private var isLandScape = false
private var imageUri: Uri? = null
private var imageMaxWidth = 0
private var imageMaxHeight = 0
private var imageProcessor: VisionImageProcessor? = null
private lateinit var imageCapture: ImageCapture
private lateinit var outputDirectory: File
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private var isFlashOn = false
private val REQUEST_CAMERA_PERMISSION = 100
private lateinit var bottomSheetDialog: BottomSheetDialog
private var chooseLanguage: Boolean = false
private lateinit var binding: ActivityStillImageBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityStillImageBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(0, systemBars.top + 26, 0, systemBars.bottom)
insets
}
Widget.makeSnackbar(this, "Photographing text for translation")
isLandScape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
savedInstanceState?.let {
imageUri = it.getParcelable(KEY_IMAGE_URI)
imageMaxWidth = it.getInt(KEY_IMAGE_MAX_WIDTH)
imageMaxHeight = it.getInt(KEY_IMAGE_MAX_HEIGHT)
selectedSize = it.getString(KEY_SELECTED_SIZE)
}
val rootView = findViewById<View>(R.id.root)
rootView.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
rootView.viewTreeObserver.removeOnGlobalLayoutListener(this)
imageMaxWidth = rootView.width
imageMaxHeight = rootView.height - findViewById<View>(R.id.control).height
if (SIZE_SCREEN == selectedSize) {
tryReloadAndDetectInImage()
}
}
})
outputDirectory = getOutputDirectory()
// 检查并启动相机
checkCameraPermission()
initView()
initClick()
}
private fun checkCameraPermission() {
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
REQUEST_CAMERA_PERMISSION
)
} else {
startCamera()
}
}
private fun initView() {
binding.photoPreview.visibility = View.VISIBLE
binding.stillSourceLanguage.text =
PreferenceLanguageUtils.getString("language_source")
binding.stillTargetLanguage.text =
PreferenceLanguageUtils.getString("language_target")
bottomSheetDialog = BottomSheetDialog(this, R.style.CustomBottomSheetDialogTheme)
bottomSheetDialog.setContentView(R.layout.bottomsheet_still_lan)
bottomSheetDialog.dismissWithAnimation = true
bottomSheetDialog.findViewById<androidx.appcompat.widget.SearchView>(R.id.ph_search)
?.let { searchView ->
// 设置关闭按钮一直显示
searchView.isIconified = false
searchView.clearFocus()
// 设置点击事件
searchView.setOnClickListener {
// 清空搜索框
searchView.setQuery("", false)
// 清除焦点
searchView.clearFocus()
// 隐藏软件盘
val imm =
this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(searchView.windowToken, 0)
// 保持关闭按钮显示
searchView.isIconified = false
}
// 设置查询文本监听器
searchView.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
(bottomSheetDialog.findViewById<RecyclerView>(R.id.list_languages)?.adapter as LanguageAdapter).filter.filter(
newText
)
return true
}
})
// 设置关闭按钮点击事件
searchView.findViewById<View>(androidx.appcompat.R.id.search_close_btn)
?.setOnClickListener {
searchView.setQuery("", false)
searchView.clearFocus()
searchView.isIconified = false // 保持关闭按钮显示
}
}
initList()
}
private fun initList() {
val languages: ArrayList<Language> = LanguagesConstants.getInstance().getList(this)
if (languages.isNotEmpty()) {
val adapter = LanguageAdapter(this, languages) { _, language ->
if (!chooseLanguage) {
PreferenceLanguageUtils.putString("language_source", language.language)
selectedMode = PreferenceLanguageUtils.getString("language_source")
tryReloadAndDetectInImage()
bottomSheetDialog.dismiss()
} else {
PreferenceLanguageUtils.putString("language_target", language.language)
tryReloadAndDetectInImage()
bottomSheetDialog.dismiss()
}
binding.stillSourceLanguage.text =
PreferenceLanguageUtils.getString("language_source")
binding.stillTargetLanguage.text =
PreferenceLanguageUtils.getString("language_target")
}
bottomSheetDialog.findViewById<RecyclerView>(R.id.list_languages)?.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
bottomSheetDialog.findViewById<RecyclerView>(R.id.list_languages)?.adapter = adapter
}
}
private fun initClick() {
binding.ivStillPic.setOnClickListener {
binding.photoPreview.visibility = View.INVISIBLE
startChooseImageIntentForResult()
}
binding.ivStillTake.setOnClickListener {
binding.photoPreview.visibility = View.VISIBLE
takePhoto()
}
binding.ivStillBack.setOnClickListener { onBackPressed() }
binding.ivStillBuling.setOnClickListener {
toggleFlash()
updateFlashButtonUI()
}
binding.stillSourceLanguage.setOnClickListener {
chooseLanguage = false
bottomSheetDialog.show()
}
binding.stillTargetLanguage.setOnClickListener {
chooseLanguage = true
bottomSheetDialog.show()
}
binding.stillExChange.setOnClickListener {
val currentSourceLanguage = PreferenceLanguageUtils.getString("language_source")
val currentTargetLanguage = PreferenceLanguageUtils.getString("language_target")
// 交换源语言和目标语言
PreferenceLanguageUtils.putString("language_source", currentTargetLanguage)
PreferenceLanguageUtils.putString("language_target", currentSourceLanguage)
// 更新界面显示
binding.stillSourceLanguage.text = currentTargetLanguage
binding.stillTargetLanguage.text = currentSourceLanguage
}
}
private fun toggleFlash() {
imageCapture.flashMode =
if (isFlashOn) ImageCapture.FLASH_MODE_OFF else ImageCapture.FLASH_MODE_ON
isFlashOn = !isFlashOn
}
private fun updateFlashButtonUI() {
binding.ivStillBuling.setImageResource(if (isFlashOn) R.drawable.ic_still_bulibuli else R.drawable.ic_still_notbuli)
}
private fun startCamera() {
val previewView = findViewById<PreviewView>(R.id.photo_preview)
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
imageCapture = ImageCapture.Builder().setFlashMode(ImageCapture.FLASH_MODE_OFF).build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun takePhoto() {
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".jpg"
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
imageUri = Uri.fromFile(photoFile)
Log.d(TAG, "Photo capture succeeded: $imageUri")
tryReloadAndDetectInImage()
}
})
}
private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply { mkdirs() }
}
return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_CAMERA_PERMISSION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
startCamera()
} else {
Toast.makeText(
this,
"Camera permission is required to use the camera",
Toast.LENGTH_SHORT
).show()
}
}
}
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume")
createImageProcessor()
tryReloadAndDetectInImage()
}
override fun onPause() {
super.onPause()
imageProcessor?.stop()
}
override fun onDestroy() {
super.onDestroy()
imageProcessor?.stop()
if (::cameraProviderFuture.isInitialized) {
cameraProviderFuture.get().unbindAll()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_IMAGE_URI, imageUri)
outState.putInt(KEY_IMAGE_MAX_WIDTH, imageMaxWidth)
outState.putInt(KEY_IMAGE_MAX_HEIGHT, imageMaxHeight)
outState.putString(KEY_SELECTED_SIZE, selectedSize)
}
private fun createImageProcessor() {
imageProcessor?.stop()
try {
imageProcessor = when (selectedMode) {
TEXT_RECOGNITION_CHINESE -> TextRecognitionProcessor(
this,
ChineseTextRecognizerOptions.Builder().build(), true, true
)
"Hindi", "Marathi", "Nepali", "Sanskrit" -> TextRecognitionProcessor(
this,
DevanagariTextRecognizerOptions.Builder().build(), true, true
)
TEXT_RECOGNITION_JAPANESE -> TextRecognitionProcessor(
this,
JapaneseTextRecognizerOptions.Builder().build(), true, true
)
TEXT_RECOGNITION_KOREAN -> TextRecognitionProcessor(
this,
KoreanTextRecognizerOptions.Builder().build(), true, true
)
else -> TextRecognitionProcessor(
this,
TextRecognizerOptions.Builder().build(), true, true
)
}
} catch (e: Exception) {
Log.e(TAG, "Can not create image processor: $selectedMode", e)
Toast.makeText(
applicationContext,
"Can not create image processor: " + e.message,
Toast.LENGTH_LONG
).show()
}
}
private fun tryReloadAndDetectInImage() {
try {
if (imageUri == null) return
if (SIZE_SCREEN == selectedSize && imageMaxWidth == 0) return
val imageBitmap =
BitmapUtils.getBitmapFromContentUri(contentResolver, imageUri) ?: return
if (selectedSize == SIZE_SCREEN) {
val targetedSize = targetedWidthHeight
val scaleFactor = max(
imageBitmap.width.toFloat() / targetedSize.first.toFloat(),
imageBitmap.height.toFloat() / targetedSize.second.toFloat()
)
val resizedBitmap = Bitmap.createScaledBitmap(
imageBitmap,
(imageBitmap.width / scaleFactor).toInt(),
(imageBitmap.height / scaleFactor).toInt(),
true
)
binding.preview.setImageBitmap(resizedBitmap)
processImage(resizedBitmap)
} else {
binding.preview.setImageBitmap(imageBitmap)
processImage(imageBitmap)
}
} catch (e: IOException) {
Log.e(TAG, "Error retrieving saved image")
imageUri = null
}
}
private val targetedWidthHeight: Pair<Int, Int>
get() {
val targetWidth = if (isLandScape) max(imageMaxWidth, imageMaxHeight) else min(
imageMaxWidth,
imageMaxHeight
)
val targetHeight = if (isLandScape) min(imageMaxWidth, imageMaxHeight) else max(
imageMaxWidth,
imageMaxHeight
)
return Pair(targetWidth, targetHeight)
}
private fun processImage(bitmap: Bitmap) {
binding.graphicOverlay.clear()
imageProcessor?.processBitmap(bitmap, binding.graphicOverlay)
}
private fun startChooseImageIntentForResult() {
val intent = Intent().apply {
type = "image/*"
action = Intent.ACTION_GET_CONTENT
}
startActivityForResult(Intent.createChooser(intent, "Select Picture"), REQUEST_CHOOSE_IMAGE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CHOOSE_IMAGE && resultCode == Activity.RESULT_OK && data != null && data.data != null) {
imageUri = data.data
tryReloadAndDetectInImage()
}
}
companion object {
private const val TAG = "StillImageActivity"
private const val KEY_IMAGE_URI = "com.google.mlkit.demo.stillImage.KEY_IMAGE_URI"
private const val KEY_IMAGE_MAX_WIDTH =
"com.google.mlkit.demo.stillImage.KEY_IMAGE_MAX_WIDTH"
private const val KEY_IMAGE_MAX_HEIGHT =
"com.google.mlkit.demo.stillImage.KEY_IMAGE_MAX_HEIGHT"
private const val KEY_SELECTED_SIZE = "com.google.mlkit.demo.stillImage.KEY_SELECTED_SIZE"
private const val SIZE_SCREEN = "w:screen"
private const val TEXT_RECOGNITION_CHINESE = "Chinese, Simplified"
private const val TEXT_RECOGNITION_JAPANESE = "Japanese"
private const val TEXT_RECOGNITION_KOREAN = "Korean"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CHOOSE_IMAGE = 1001
}
}