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 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(R.id.root) rootView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { rootView.viewTreeObserver.removeOnGlobalLayoutListener(this) imageMaxWidth = rootView.width imageMaxHeight = rootView.height - findViewById(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(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(R.id.list_languages)?.adapter as LanguageAdapter).filter.filter( newText ) return true } }) // 设置关闭按钮点击事件 searchView.findViewById(androidx.appcompat.R.id.search_close_btn) ?.setOnClickListener { searchView.setQuery("", false) searchView.clearFocus() searchView.isIconified = false // 保持关闭按钮显示 } } initList() } private fun initList() { val languages: ArrayList = 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(R.id.list_languages)?.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) bottomSheetDialog.findViewById(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(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, 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 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 } }