diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index cca7557..557f0e3 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -13,6 +13,9 @@ + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8f20281..f09406f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,6 +27,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("debug") } } compileOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b84693..32678e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,6 +50,11 @@ android:name=".FullScreenImageActivity" android:exported="false" android:theme="@style/Theme.AnimalRating" /> + + diff --git a/app/src/main/java/com/example/animalrating/CameraProcessor.kt b/app/src/main/java/com/example/animalrating/CameraProcessor.kt index c2ac78c..5335e0c 100644 --- a/app/src/main/java/com/example/animalrating/CameraProcessor.kt +++ b/app/src/main/java/com/example/animalrating/CameraProcessor.kt @@ -2,6 +2,7 @@ package com.example.animalrating import android.Manifest import android.content.ContentValues +import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -12,10 +13,12 @@ import android.os.Bundle import android.provider.MediaStore import android.util.Log import android.util.Size +import android.view.View import android.widget.ImageView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException @@ -49,11 +52,15 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { private var isPhotoTaken = false private var matchThreshold = 75 private var algorithm = HomeActivity.ALGORITHM_HAMMING + private var isAutoCapture = true + private var isMaskDisplayEnabled = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + StringProvider.initialize(this) + cowName = intent.getStringExtra("COW_NAME") orientation = intent.getStringExtra("ORIENTATION") @@ -61,6 +68,8 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE) matchThreshold = prefs.getInt("THRESHOLD", 75) algorithm = prefs.getString("ALGORITHM", HomeActivity.ALGORITHM_HAMMING) ?: HomeActivity.ALGORITHM_HAMMING + isAutoCapture = prefs.getBoolean(HomeActivity.PREF_AUTO_CAPTURE, true) + isMaskDisplayEnabled = prefs.getBoolean(HomeActivity.PREF_MASK_DISPLAY, false) // Set orientation based on selected view if (orientation == "front" || orientation == "back") { @@ -77,16 +86,56 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { findViewById(R.id.btnExit).setOnClickListener { finish() } + + val btnShutter = findViewById(R.id.btnShutter) + val btnToggle = findViewById(R.id.btnToggleCaptureMode) + + btnShutter.setOnClickListener { + takePhoto() + } + + updateCaptureModeUI() + + btnToggle.setOnClickListener { + isAutoCapture = !isAutoCapture + // Save preference for persistence if desired, or just toggle for session + prefs.edit().putBoolean(HomeActivity.PREF_AUTO_CAPTURE, isAutoCapture).apply() + updateCaptureModeUI() + } frameProcessor = FrameProcessor() val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0) overlay.setSilhouette(silhouetteId) - loadSavedMask() + // Need to wait for layout to get width/height for scaling mask correctly + savedMaskOverlay.post { + loadSavedMask() + } requestPermissionLauncher.launch(Manifest.permission.CAMERA) } + + private fun updateCaptureModeUI() { + val btnShutter = findViewById(R.id.btnShutter) + val btnToggle = findViewById(R.id.btnToggleCaptureMode) + + if (isAutoCapture) { + btnShutter.visibility = View.GONE + // Auto Mode: Eye Icon, Dark Background + btnToggle.setIconResource(android.R.drawable.ic_menu_view) + btnToggle.setIconTintResource(android.R.color.white) + btnToggle.setBackgroundColor(Color.parseColor("#6D4C41")) + btnToggle.alpha = 1.0f + } else { + btnShutter.visibility = View.VISIBLE + // Manual Mode: Eye Icon (Grey/Transparent) - Requested to keep Eye icon + btnToggle.setIconResource(android.R.drawable.ic_menu_view) + btnToggle.setIconTintResource(android.R.color.darker_gray) + btnToggle.setBackgroundColor(Color.TRANSPARENT) + btnToggle.alpha = 0.7f + } + } private fun takePhoto() { if (isPhotoTaken) return @@ -96,15 +145,22 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { val name = cowName ?: "unknown" val side = orientation ?: "unknown" + val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0) // Find current count for this cow and orientation - val existingFiles = filesDir.listFiles { _, fname -> + val cowFolder = StorageUtils.getCowImageFolder(name) + + val existingFiles = cowFolder.listFiles { _, fname -> fname.startsWith("${name}_${side}_") && fname.endsWith(".jpg") } val count = (existingFiles?.size ?: 0) + 1 + if (!cowFolder.exists()) { + cowFolder.mkdirs() + } + val filename = "${name}_${side}_${count}.jpg" - val file = File(filesDir, filename) + val file = File(cowFolder, filename) val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build() @@ -113,8 +169,8 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(exc: ImageCaptureException) { - Log.e("MainActivity", "Photo capture failed: ${exc.message}", exc) - Toast.makeText(baseContext, "Photo capture failed", Toast.LENGTH_SHORT).show() + Log.e("CameraProcessor", "Photo capture failed: ${exc.message}", exc) + Toast.makeText(baseContext, StringProvider.getString("toast_capture_failed"), Toast.LENGTH_SHORT).show() isPhotoTaken = false } @@ -133,17 +189,31 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) } } + + val retakePath = intent.getStringExtra("RETAKE_IMAGE_PATH") + if (!retakePath.isNullOrEmpty()) { + val oldFile = File(retakePath) + if (oldFile.exists()) { + oldFile.delete() + } + } - // Save to public gallery - saveToGallery(file) - - val msg = "Saved as $filename" + val msg = "${StringProvider.getString("toast_saved_as")} $filename" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() + + // Navigate to FullScreenImageActivity + val intent = Intent(this@CameraProcessor, FullScreenImageActivity::class.java) + intent.putExtra("IMAGE_PATH", file.absolutePath) + intent.putExtra("ALLOW_RETAKE", true) + intent.putExtra("COW_NAME", cowName) + intent.putExtra("ORIENTATION", orientation) + intent.putExtra("SILHOUETTE_ID", silhouetteId) + startActivity(intent) finish() } catch (e: Exception) { - Log.e("MainActivity", "Error saving image", e) - Toast.makeText(baseContext, "Error saving image", Toast.LENGTH_SHORT).show() + Log.e("CameraProcessor", "Error saving image", e) + Toast.makeText(baseContext, StringProvider.getString("toast_error_saving_image"), Toast.LENGTH_SHORT).show() isPhotoTaken = false } } @@ -152,38 +222,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { } private fun saveToGallery(file: File) { - val values = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, file.name) - put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/AnimalRating") - put(MediaStore.Images.Media.IS_PENDING, 1) - } - } - - val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) - } else { - MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } - - try { - val uri = contentResolver.insert(collection, values) - uri?.let { - contentResolver.openOutputStream(it)?.use { out -> - FileInputStream(file).use { input -> - input.copyTo(out) - } - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - values.clear() - values.put(MediaStore.Images.Media.IS_PENDING, 0) - contentResolver.update(it, values, null, null) - } - } - } catch (e: Exception) { - Log.e("CameraProcessor", "Error saving to gallery", e) - } + // Removed } private fun loadSavedMask() { @@ -195,17 +234,50 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { try { val savedBitmap = BitmapFactory.decodeFile(file.absolutePath) - // Apply green color filter for visualization (on original size) - val greenMask = applyGreenColor(savedBitmap) - savedMaskOverlay.setImageBitmap(greenMask) - savedMaskOverlay.alpha = 0.5f - - // Scale to 640x480 (or flipped for portrait) for comparison - val isPortrait = (side == "front" || side == "back") - val width = if (isPortrait) 480 else 640 - val height = if (isPortrait) 640 else 480 - - savedMaskBitmap = Bitmap.createScaledBitmap(savedBitmap, width, height, true) + if (savedBitmap != null) { + // Apply green color filter for visualization + if (isMaskDisplayEnabled) { + val greenMask = applyGreenColor(savedBitmap) + + // Calculate scale to match FIT_CENTER logic of SilhouetteOverlay + val viewW = savedMaskOverlay.width.toFloat() + val viewH = savedMaskOverlay.height.toFloat() + + if (viewW > 0 && viewH > 0) { + val bmpW = greenMask.width.toFloat() + val bmpH = greenMask.height.toFloat() + + val scale = kotlin.math.min(viewW / bmpW, viewH / bmpH) + val scaledW = (bmpW * scale).toInt() + val scaledH = (bmpH * scale).toInt() + + if (scaledW > 0 && scaledH > 0) { + val scaledBitmap = Bitmap.createScaledBitmap(greenMask, scaledW, scaledH, true) + savedMaskOverlay.setImageBitmap(scaledBitmap) + savedMaskOverlay.scaleType = ImageView.ScaleType.FIT_CENTER // Ensure it centers + } + } else { + // Fallback if view size not ready yet, though post() should handle it + savedMaskOverlay.setImageBitmap(greenMask) + } + + savedMaskOverlay.alpha = 0.5f + } else { + savedMaskOverlay.setImageDrawable(null) + } + + // Prepare mask for analysis (640x480 target) + // We should ideally match the visible part of the preview + // For simplicity, keeping original scaling logic for analysis as it might depend on full frame + // However, if the overlay is FIT_CENTER, the analysis should likely respect that aspect ratio too + // But for now, let's just fix the visual overlay as requested. + + val isPortrait = (side == "front" || side == "back") + val width = if (isPortrait) 480 else 640 + val height = if (isPortrait) 640 else 480 + + savedMaskBitmap = Bitmap.createScaledBitmap(savedBitmap, width, height, true) + } } catch (e: Exception) { Log.e("CameraProcessor", "Error loading saved mask", e) @@ -269,6 +341,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { }, ContextCompat.getMainExecutor(this)) } + @ExperimentalGetImage override fun onFrame(imageProxy: ImageProxy) { if (isPhotoTaken) { imageProxy.close() @@ -282,10 +355,14 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { runOnUiThread { if (result.mask != null) { currentMask = result.mask - segmentationOverlay.setImageBitmap(result.mask) + if (isMaskDisplayEnabled) { + segmentationOverlay.setImageBitmap(result.mask) + } else { + segmentationOverlay.setImageDrawable(null) + } } } - if (result.isMatch) { + if (isAutoCapture && result.isMatch) { takePhoto() } } @@ -293,4 +370,4 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener { Log.e("CameraProcessor", "Frame processing error", e) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/animalrating/CowSelectionActivity.kt b/app/src/main/java/com/example/animalrating/CowSelectionActivity.kt index 1cb5ac0..5a58df8 100644 --- a/app/src/main/java/com/example/animalrating/CowSelectionActivity.kt +++ b/app/src/main/java/com/example/animalrating/CowSelectionActivity.kt @@ -10,7 +10,6 @@ import android.view.View import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView import android.widget.Button -import android.widget.GridLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.RadioButton @@ -34,51 +33,21 @@ class CowSelectionActivity : AppCompatActivity() { private var currentCowName: String? = null private lateinit var imagesContainer: LinearLayout private val storagePermissionCode = 101 + private val orientationViews = mutableMapOf() + private val initialImagePaths = mutableSetOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_cow_selection) - // Initialize StringProvider StringProvider.initialize(this) + setupUIStrings() - // Set UI text from StringProvider - findViewById(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_cow_selection") - findViewById(R.id.tvAddCowDetails)?.text = StringProvider.getString("title_add_cow_details") - - findViewById(R.id.tilSpecies)?.hint = StringProvider.getString("hint_species") - findViewById(R.id.tilBreed)?.hint = StringProvider.getString("hint_breed") - findViewById(R.id.tilAge)?.hint = StringProvider.getString("hint_age") - findViewById(R.id.tilMilk)?.hint = StringProvider.getString("hint_milk_yield") - findViewById(R.id.tilCalving)?.hint = StringProvider.getString("hint_calving_number") - findViewById(R.id.tilDescription)?.hint = StringProvider.getString("hint_description") - - findViewById(R.id.tvReproductiveStatus)?.text = StringProvider.getString("label_reproductive_status") - findViewById(R.id.rbPregnant)?.text = StringProvider.getString("radio_pregnant") - findViewById(R.id.rbCalved)?.text = StringProvider.getString("radio_calved") - findViewById(R.id.rbNone)?.text = StringProvider.getString("radio_none") - - findViewById(R.id.tvUploadPhotos)?.text = StringProvider.getString("label_upload_photos") - findViewById