Compare commits

...

No commits in common. "animal-rating-android-studio" and "living-ai-rater" have entirely different histories.

52 changed files with 1510 additions and 1546 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -4,17 +4,6 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-11-24T18:35:29.331278100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=10BF45100J001X5" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="release">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>

View File

@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -5,13 +5,11 @@ plugins {
}
android {
namespace = "com.example.animalrating"
compileSdk {
version = release(36)
}
namespace = "com.example.livingai"
compileSdk = 36
defaultConfig {
applicationId = "com.example.animalrating"
applicationId = "com.example.livingai"
minSdk = 24
targetSdk = 36
versionCode = 1
@ -27,7 +25,6 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
@ -39,23 +36,22 @@ android {
}
buildFeatures {
compose = true
viewBinding = true
}
}
dependencies {
val cameraxVersion = "1.5.1"
val cameraxVersion = "1.5.0-alpha03"
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.camera:camera-core:$cameraxVersion")
implementation("androidx.camera:camera-camera2:$cameraxVersion")
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
implementation("androidx.camera:camera-view:$cameraxVersion")
implementation("androidx.camera:camera-mlkit-vision:$cameraxVersion")
implementation("androidx.camera:camera-core:${cameraxVersion}")
implementation("androidx.camera:camera-camera2:${cameraxVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraxVersion}")
implementation("androidx.camera:camera-view:${cameraxVersion}")
implementation("androidx.camera:camera-mlkit-vision:${cameraxVersion}")
// ML Kit Object Detection
implementation("com.google.mlkit:object-detection:17.0.2")
// ML Kit Subject Segmentation (Google Play Services version)
implementation("com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1")
implementation(libs.androidx.core.ktx)
@ -66,6 +62,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.animalrating", appContext.packageName)
assertEquals("com.example.livingai", appContext.packageName)
}
}

View File

@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
@ -16,46 +16,48 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AnimalRating"
android:requestLegacyExternalStorage="true">
android:theme="@style/Theme.LivingAI">
<activity
android:name=".HomeActivity"
android:name=".ui.page.HomeActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.AnimalRating">
android:label="@string/title_home"
android:theme="@style/Theme.LivingAI">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".CowSelectionActivity"
android:name=".ui.page.GalleryActivity"
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
<activity
android:name=".CameraProcessor"
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
<activity
android:name=".GalleryActivity"
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
android:label="@string/title_gallery"
android:theme="@style/Theme.LivingAI" />
<activity
android:name=".FullScreenImageActivity"
android:name=".ui.page.AddAnimalProfileActivity"
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
android:label="@string/title_add_cow_details"
android:theme="@style/Theme.LivingAI"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".RatingActivity"
android:name=".ui.page.RatingActivity"
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
android:label="@string/title_rate_cow"
android:theme="@style/Theme.LivingAI" />
<activity
android:name=".ui.page.FullScreenImageActivity"
android:exported="false"
android:theme="@style/Theme.LivingAI" />
<activity
android:name=".camera.CameraActivity"
android:exported="false"
android:theme="@style/Theme.LivingAI" />
</application>
</manifest>

View File

@ -1,373 +0,0 @@
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
import android.graphics.Color
import android.graphics.Matrix
import android.os.Build
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
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import com.example.animalrating.ml.CowAnalyzer
import com.example.animalrating.ui.SilhouetteOverlay
import com.google.android.material.button.MaterialButton
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.concurrent.Executors
class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
private lateinit var previewView: PreviewView
private lateinit var overlay: SilhouetteOverlay
private lateinit var segmentationOverlay: ImageView
private lateinit var savedMaskOverlay: ImageView
private var imageCapture: ImageCapture? = null
private lateinit var frameProcessor: FrameProcessor
private val cameraExecutor = Executors.newSingleThreadExecutor()
private var cowName: String? = null
private var orientation: String? = null
private var currentMask: Bitmap? = null
private var savedMaskBitmap: Bitmap? = null
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")
// Load settings
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") {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
previewView = findViewById(R.id.cameraPreview)
overlay = findViewById(R.id.silhouetteOverlay)
segmentationOverlay = findViewById(R.id.segmentationOverlay)
savedMaskOverlay = findViewById(R.id.savedMaskOverlay)
findViewById<MaterialButton>(R.id.btnExit).setOnClickListener {
finish()
}
val btnShutter = findViewById<MaterialButton>(R.id.btnShutter)
val btnToggle = findViewById<MaterialButton>(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)
// 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<MaterialButton>(R.id.btnShutter)
val btnToggle = findViewById<MaterialButton>(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
isPhotoTaken = true
val imageCapture = imageCapture ?: return
val name = cowName ?: "unknown"
val side = orientation ?: "unknown"
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
// Find current count for this cow and orientation
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(cowFolder, filename)
val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e("CameraProcessor", "Photo capture failed: ${exc.message}", exc)
Toast.makeText(baseContext, StringProvider.getString("toast_capture_failed"), Toast.LENGTH_SHORT).show()
isPhotoTaken = false
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
// Correct rotation based on current orientation
try {
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
val matrix = Matrix()
val rotation = if (orientation == "front" || orientation == "back") 90f else 0f
if (rotation != 0f) {
matrix.postRotate(rotation)
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
FileOutputStream(file).use { out ->
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()
}
}
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("CameraProcessor", "Error saving image", e)
Toast.makeText(baseContext, StringProvider.getString("toast_error_saving_image"), Toast.LENGTH_SHORT).show()
isPhotoTaken = false
}
}
}
)
}
private fun saveToGallery(file: File) {
// Removed
}
private fun loadSavedMask() {
val side = orientation ?: "unknown"
val filename = "${side}_mask.png"
val file = File(filesDir, filename)
if (file.exists()) {
try {
val savedBitmap = BitmapFactory.decodeFile(file.absolutePath)
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)
}
}
}
private fun applyGreenColor(original: Bitmap): Bitmap {
val width = original.width
val height = original.height
val pixels = IntArray(width * height)
original.getPixels(pixels, 0, width, 0, 0, width, height)
for (i in pixels.indices) {
val alpha = (pixels[i] shr 24) and 0xff
if (alpha > 10) {
// Set to Green with original alpha
pixels[i] = Color.argb(alpha, 0, 255, 0)
}
}
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) startCamera()
}
private fun startCamera() {
val providerFuture = ProcessCameraProvider.getInstance(this)
providerFuture.addListener({
val cameraProvider = providerFuture.get()
val preview = androidx.camera.core.Preview.Builder().build()
preview.surfaceProvider = previewView.surfaceProvider
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
val analyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 480))
.setBackpressureStrategy(
ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
)
.build()
.also {
it.setAnalyzer(cameraExecutor, CowAnalyzer(this))
}
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this,
androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture,
analyzer
)
}, ContextCompat.getMainExecutor(this))
}
@ExperimentalGetImage
override fun onFrame(imageProxy: ImageProxy) {
if (isPhotoTaken) {
imageProxy.close()
return
}
val isPortrait = (orientation == "front" || orientation == "back")
frameProcessor.processFrame(imageProxy, savedMaskBitmap, isPortrait, matchThreshold, algorithm)
.addOnSuccessListener { result ->
runOnUiThread {
if (result.mask != null) {
currentMask = result.mask
if (isMaskDisplayEnabled) {
segmentationOverlay.setImageBitmap(result.mask)
} else {
segmentationOverlay.setImageDrawable(null)
}
}
}
if (isAutoCapture && result.isMatch) {
takePhoto()
}
}
.addOnFailureListener { e ->
Log.e("CameraProcessor", "Frame processing error", e)
}
}
}

View File

@ -1,273 +0,0 @@
package com.example.animalrating
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Matrix
import android.util.Log
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskCompletionSource
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.sqrt
data class SegmentationResult(
val mask: Bitmap?,
val isMatch: Boolean
)
class FrameProcessor {
private val isProcessing = AtomicBoolean(false)
private val processingExecutor = Executors.newSingleThreadExecutor()
private val options = SubjectSegmenterOptions.Builder()
.enableMultipleSubjects(
SubjectSegmenterOptions.SubjectResultOptions.Builder()
.enableConfidenceMask()
.build()
)
.build()
private val segmenter = SubjectSegmentation.getClient(options)
@ExperimentalGetImage
fun processFrame(
imageProxy: ImageProxy,
savedMask: Bitmap?,
isPortrait: Boolean,
thresholdPercent: Int = 75,
algorithm: String = HomeActivity.ALGORITHM_HAMMING
): Task<SegmentationResult> {
val taskCompletionSource = TaskCompletionSource<SegmentationResult>()
if (!isProcessing.compareAndSet(false, true)) {
imageProxy.close()
taskCompletionSource.setResult(SegmentationResult(null, false))
return taskCompletionSource.task
}
val mediaImage = imageProxy.image
if (mediaImage == null) {
isProcessing.set(false)
imageProxy.close()
taskCompletionSource.setResult(SegmentationResult(null, false))
return taskCompletionSource.task
}
val inputImage = InputImage.fromMediaImage(mediaImage, 0)
segmenter.process(inputImage)
.addOnSuccessListener(processingExecutor) { result ->
var bitmapMask: Bitmap? = null
val subject = result.subjects.firstOrNull()
val mask = subject?.confidenceMask
if (mask != null) {
val startX = subject.startX
val startY = subject.startY
val maskWidth = subject.width
val maskHeight = subject.height
val fullWidth = inputImage.width
val fullHeight = inputImage.height
if (mask.remaining() >= maskWidth * maskHeight) {
val colors = IntArray(fullWidth * fullHeight)
mask.rewind()
for (y in 0 until maskHeight) {
for (x in 0 until maskWidth) {
if (mask.get() > 0.5f) {
val destX = startX + x
val destY = startY + y
if (destX < fullWidth && destY < fullHeight) {
colors[destY * fullWidth + destX] = Color.argb(180, 255, 0, 255)
}
}
}
}
val rawBitmap = Bitmap.createBitmap(colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888)
// Rotate and Scale if needed
bitmapMask = if (isPortrait) {
try {
val matrix = Matrix()
// Rotate 90 degrees
matrix.postRotate(90f)
// Assuming previous scaling logic might still be relevant if masks are consistently off
// But generally, we trust the geometry.
// If scaling up is happening unexpectedly, one could use:
// matrix.postScale(0.9f, 0.9f)
// For now, stick to rotation unless requested otherwise.
Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
} catch (e: Exception) {
Log.e("FrameProcessor", "Error rotating mask", e)
rawBitmap
}
} else {
rawBitmap
}
} else {
Log.e("FrameProcessor", "Mask buffer size mismatch")
}
}
// Calculate match
var isMatch = false
if (bitmapMask != null && savedMask != null) {
// Scale current mask to match saved mask dimensions (640x480) for comparison
val comparisonMask = if (bitmapMask.width != savedMask.width || bitmapMask.height != savedMask.height) {
try {
Bitmap.createScaledBitmap(bitmapMask, savedMask.width, savedMask.height, true)
} catch (e: Exception) {
Log.e("FrameProcessor", "Error scaling mask for comparison", e)
null
}
} else {
bitmapMask
}
if (comparisonMask != null) {
// Map display strings to internal algorithm keys/logic
val algoKey = when(algorithm) {
StringProvider.getString("algo_euclidean") -> HomeActivity.ALGORITHM_EUCLIDEAN
StringProvider.getString("algo_jaccard") -> HomeActivity.ALGORITHM_JACCARD
else -> {
if (algorithm == HomeActivity.ALGORITHM_EUCLIDEAN || algorithm == HomeActivity.ALGORITHM_JACCARD) algorithm
else HomeActivity.ALGORITHM_HAMMING
}
}
isMatch = when (algoKey) {
HomeActivity.ALGORITHM_EUCLIDEAN -> calculateEuclideanDistance(savedMask, comparisonMask, thresholdPercent)
HomeActivity.ALGORITHM_JACCARD -> calculateJaccardSimilarity(savedMask, comparisonMask, thresholdPercent)
else -> calculateHammingDistance(savedMask, comparisonMask, thresholdPercent)
}
}
}
taskCompletionSource.setResult(SegmentationResult(bitmapMask, isMatch))
}
.addOnFailureListener { e ->
Log.e("FrameProcessor", "Subject Segmentation failed", e)
taskCompletionSource.setException(e)
}
.addOnCompleteListener { _ ->
isProcessing.set(false)
imageProxy.close()
}
return taskCompletionSource.task
}
private fun calculateHammingDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
if (mask1.width != mask2.width || mask1.height != mask2.height) {
return false
}
val width = mask1.width
val height = mask1.height
val pixels1 = IntArray(width * height)
val pixels2 = IntArray(width * height)
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
var distance = 0
for (i in pixels1.indices) {
val isSet1 = (pixels1[i] ushr 24) > 0
val isSet2 = (pixels2[i] ushr 24) > 0
if (isSet1 != isSet2) {
distance++
}
}
val totalPixels = width * height
val validThreshold = thresholdPercent.coerceIn(1, 100)
val allowedDistance = (totalPixels.toLong() * (100 - validThreshold)) / 100
return distance <= allowedDistance
}
private fun calculateEuclideanDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
if (mask1.width != mask2.width || mask1.height != mask2.height) {
return false
}
val width = mask1.width
val height = mask1.height
val pixels1 = IntArray(width * height)
val pixels2 = IntArray(width * height)
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
var sumSq = 0L
for (i in pixels1.indices) {
// Simple binary comparison for Euclidean distance on masks
// Treat existence of pixel as 255, non-existence as 0
val val1 = if ((pixels1[i] ushr 24) > 0) 255 else 0
val val2 = if ((pixels2[i] ushr 24) > 0) 255 else 0
val diff = val1 - val2
sumSq += diff * diff
}
val euclideanDistance = sqrt(sumSq.toDouble())
val maxDistance = sqrt((width * height).toDouble()) * 255.0
val validThreshold = thresholdPercent.coerceIn(1, 100)
val allowedDistance = maxDistance * (100 - validThreshold) / 100.0
return euclideanDistance <= allowedDistance
}
private fun calculateJaccardSimilarity(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
if (mask1.width != mask2.width || mask1.height != mask2.height) {
return false
}
val width = mask1.width
val height = mask1.height
val pixels1 = IntArray(width * height)
val pixels2 = IntArray(width * height)
mask1.getPixels(pixels1, 0, width, 0, 0, width, height)
mask2.getPixels(pixels2, 0, width, 0, 0, width, height)
var intersection = 0
var union = 0
for (i in pixels1.indices) {
// mask1 is Saved Mask (Likely Inverted in StorageUtils: Subject is Transparent/0, Background is Black/255)
// But check how it's loaded. In CameraProcessor: loadSavedMask() -> decodeFile().
// If we assume saved mask is inverted (subject transparent), then alpha < 128 is subject.
val alpha1 = (pixels1[i] ushr 24) and 0xFF
val isSubject1 = alpha1 < 128
// mask2 is Live Mask (Subject is Magenta/180 alpha)
val alpha2 = (pixels2[i] ushr 24) and 0xFF
val isSubject2 = alpha2 > 0
if (isSubject1 && isSubject2) {
intersection++
}
if (isSubject1 || isSubject2) {
union++
}
}
if (union == 0) return false
val jaccardIndex = (intersection.toDouble() / union.toDouble()) * 100
return jaccardIndex >= thresholdPercent
}
}

View File

@ -1,101 +0,0 @@
package com.example.animalrating
import android.content.Context
import org.json.JSONObject
import java.io.InputStream
object StringProvider {
private var stringData: JSONObject? = null
private var currentLanguage = "English"
private const val DEFAULT_LANGUAGE = "English"
fun initialize(context: Context) {
if (stringData == null) {
try {
val inputStream: InputStream = context.resources.openRawResource(R.raw.strings)
val jsonString = inputStream.bufferedReader().use { it.readText() }
stringData = JSONObject(jsonString)
// Load saved language
val prefs = context.getSharedPreferences("AnimalRatingPrefs", Context.MODE_PRIVATE)
currentLanguage = prefs.getString("LANGUAGE", DEFAULT_LANGUAGE) ?: DEFAULT_LANGUAGE
} catch (e: Exception) {
e.printStackTrace()
// Handle error, maybe load default strings or log
}
}
}
fun setLanguage(language: String, context: Context) {
currentLanguage = language
// Save selected language
val prefs = context.getSharedPreferences("AnimalRatingPrefs", Context.MODE_PRIVATE)
prefs.edit().putString("LANGUAGE", language).apply()
}
fun getLanguages(): List<String> {
return stringData?.keys()?.asSequence()?.toList() ?: listOf(DEFAULT_LANGUAGE)
}
fun getString(key: String): String {
return getStringForLanguage(key, currentLanguage)
}
fun getStringEnglish(key: String): String {
return getStringForLanguage(key, DEFAULT_LANGUAGE)
}
private fun getStringForLanguage(key: String, language: String): String {
return try {
stringData?.getJSONObject(language)?.getString(key) ?: ""
} catch (e: Exception) {
// Fallback to English if key not found in current language
try {
if (language != DEFAULT_LANGUAGE) {
stringData?.getJSONObject(DEFAULT_LANGUAGE)?.getString(key) ?: ""
} else {
""
}
} catch (e2: Exception) {
"" // Return empty if not found anywhere
}
}
}
fun getKeyForValue(value: String): String? {
// Helper to find the key for a given localized value in current language
// This is expensive, but useful if we need to reverse lookup
val langData = stringData?.optJSONObject(currentLanguage) ?: return null
val keys = langData.keys()
while (keys.hasNext()) {
val key = keys.next()
if (langData.getString(key) == value) {
return key
}
}
// Also check English just in case it was stored as English
val englishData = stringData?.optJSONObject(DEFAULT_LANGUAGE) ?: return null
val engKeys = englishData.keys()
while (engKeys.hasNext()) {
val key = engKeys.next()
if (englishData.getString(key) == value) {
return key
}
}
return null
}
fun getKeyForEnglishValue(value: String): String? {
val englishData = stringData?.optJSONObject(DEFAULT_LANGUAGE) ?: return null
val engKeys = englishData.keys()
while (engKeys.hasNext()) {
val key = engKeys.next()
if (englishData.getString(key) == value) {
return key
}
}
return null
}
}

View File

@ -1,43 +0,0 @@
package com.example.animalrating.ml
import android.graphics.*
import androidx.camera.core.ImageProxy
import java.io.ByteArrayOutputStream
/**
* Convert ImageProxy YUV_420_888 to Bitmap
*/
fun ImageProxy.toBitmap(): Bitmap {
val yBuffer = planes[0].buffer // Y
val uBuffer = planes[1].buffer // U
val vBuffer = planes[2].buffer // V
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
// U and V are swapped, so we place V first then U
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
val yuvImage = YuvImage(
nv21,
ImageFormat.NV21,
width,
height,
null
)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(
Rect(0, 0, width, height),
90,
out
)
val jpegBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
}

View File

@ -1,24 +0,0 @@
package com.example.animalrating.ui
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class MaskOverlay(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var maskBitmap: Bitmap? = null
private var matrix: Matrix = Matrix()
fun updateMask(bitmap: Bitmap?, transform: Matrix?) {
maskBitmap = bitmap
matrix = transform ?: Matrix()
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val bmp = maskBitmap ?: return
canvas.drawBitmap(bmp, matrix, null)
}
}

View File

@ -1,14 +1,13 @@
package com.example.animalrating.ml
package com.example.livingai.analysis
import android.graphics.Bitmap
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
class CowAnalyzer(
private val listener: CowListener
class Analyzer(
private val listener: AnalysisListener
) : ImageAnalysis.Analyzer {
interface CowListener {
interface AnalysisListener {
fun onFrame(imageProxy: ImageProxy)
}

View File

@ -0,0 +1,25 @@
package com.example.livingai.analysis
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Matrix
import android.util.Log
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import com.example.livingai.commons.Constants
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskCompletionSource
import com.google.mlkit.vision.common.InputImage
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
/**
* Responsible for processing frames from the camera.
* It prepares the frame (e.g. orientation) and passes it to SegmentProcessor.
*/
class FrameProcessor {
fun prepareInputImage(imageProxy: ImageProxy, rotationDegrees: Int): InputImage? {
val mediaImage = imageProxy.image ?: return null
return InputImage.fromMediaImage(mediaImage, rotationDegrees)
}
}

View File

@ -0,0 +1,48 @@
package com.example.livingai.analysis
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.util.Log
import com.example.livingai.commons.Constants
import java.io.File
class SavedMaskProcessor(private val context: Context) {
fun loadSavedMask(orientation: String): Bitmap? {
val filename = "${orientation}_mask.png"
val file = File(context.filesDir, filename)
var savedMaskBitmap: Bitmap? = null
if (file.exists()) {
try {
val savedBitmap = BitmapFactory.decodeFile(file.absolutePath)
if (savedBitmap != null) {
//TODO: Added for debug can be removed safely
savedMaskBitmap = applyGreenColor(savedBitmap)
}
} catch (e: Exception) {
Log.e("SavedMaskProcessor", "Error loading saved mask", e)
}
}
return savedMaskBitmap
}
//TODO: Added for debug can be removed safely
private fun applyGreenColor(original: Bitmap): Bitmap {
val width = original.width
val height = original.height
val pixels = IntArray(width * height)
original.getPixels(pixels, 0, width, 0, 0, width, height)
for (i in pixels.indices) {
val alpha = (pixels[i] shr 24) and 0xff
if (alpha > 10) {
pixels[i] = Color.argb(alpha, 0, 255, 0)
}
}
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
}
}

View File

@ -0,0 +1,265 @@
package com.example.livingai.analysis
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Matrix
import android.util.Log
import com.example.livingai.commons.Constants
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskCompletionSource
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
data class SegmentationResult(
val mask: Bitmap?,
val isMatch: Boolean
)
class SegmentProcessor {
private val isProcessing = AtomicBoolean(false)
private val processingExecutor = Executors.newSingleThreadExecutor()
private val metrics = Resources.getSystem().displayMetrics
private val screenW = metrics.widthPixels
private val screenH = metrics.heightPixels
private val options = SubjectSegmenterOptions.Builder()
.enableMultipleSubjects(
SubjectSegmenterOptions.SubjectResultOptions.Builder()
.enableConfidenceMask()
.build()
)
.build()
private val segmenter = SubjectSegmentation.getClient(options)
fun process(
inputImage: InputImage,
savedMask: Bitmap?,
thresholdPercent: Int = 75,
algorithm: String = Constants.ALGORITHM_HAMMING
): Task<SegmentationResult> {
val tcs = TaskCompletionSource<SegmentationResult>()
if (!isProcessing.compareAndSet(false, true)) {
tcs.setResult(SegmentationResult(null, false))
return tcs.task
}
segmenter.process(inputImage)
.addOnSuccessListener(processingExecutor) { result ->
var bitmapMask: Bitmap? = null
val subject = result.subjects.firstOrNull()
val mask = subject?.confidenceMask
if (mask != null) {
val startX = subject.startX
val startY = subject.startY
val maskWidth = subject.width
val maskHeight = subject.height
val fullWidth = inputImage.width
val fullHeight = inputImage.height
if (mask.remaining() >= maskWidth * maskHeight) {
val colors = IntArray(fullWidth * fullHeight)
mask.rewind()
// Build full-resolution mask
for (y in 0 until maskHeight) {
for (x in 0 until maskWidth) {
if (mask.get() > 0.5f) {
val dx = startX + x
val dy = startY + y
if (dx < fullWidth && dy < fullHeight) {
colors[dy * fullWidth + dx] =
Color.argb(180, 255, 0, 255)
}
}
}
}
val rawBitmap = Bitmap.createBitmap(
colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888
)
// Rotate to match screen orientation if needed (so segmentation mask is upright)
if (inputImage.rotationDegrees != 0) {
val matrix = Matrix()
matrix.postRotate(inputImage.rotationDegrees.toFloat())
bitmapMask = Bitmap.createBitmap(
rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true
)
} else {
bitmapMask = rawBitmap
}
}
}
var isMatch = false
if (bitmapMask != null && savedMask != null) {
//To consider all kinds of phones with aspect ratio and screen size
val (scaledSavedMask, scaledSegmentedMask) = scaleBothToScreenAndCrop(savedMask, bitmapMask)
isMatch = when (algorithm) {
Constants.ALGORITHM_EUCLIDEAN ->
calculateEuclideanDistance(scaledSavedMask, scaledSegmentedMask, thresholdPercent)
Constants.ALGORITHM_JACCARD ->
calculateJaccardSimilarity(scaledSavedMask, scaledSegmentedMask, thresholdPercent)
else ->
calculateHammingDistance(scaledSavedMask, scaledSegmentedMask, thresholdPercent)
}
}
tcs.setResult(SegmentationResult(bitmapMask, isMatch))
}
.addOnFailureListener {
Log.e("SegmentProcessor", "Segmentation failed", it)
tcs.setException(it)
}
.addOnCompleteListener {
isProcessing.set(false)
}
return tcs.task
}
private fun calculateHammingDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
if (mask1.width != mask2.width || mask1.height != mask2.height) return false
val width = mask1.width
val height = mask1.height
val p1 = IntArray(width * height)
val p2 = IntArray(width * height)
mask1.getPixels(p1, 0, width, 0, 0, width, height)
mask2.getPixels(p2, 0, width, 0, 0, width, height)
var distance = 0
for (i in p1.indices) {
val a = (p1[i] ushr 24) > 0
val b = (p2[i] ushr 24) > 0
if (a != b) distance++
}
val total = width * height
val allowed = total * (100 - thresholdPercent) / 100
return distance <= allowed
}
private fun calculateEuclideanDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
if (mask1.width != mask2.width || mask1.height != mask2.height) return false
val width = mask1.width
val height = mask1.height
val p1 = IntArray(width * height)
val p2 = IntArray(width * height)
mask1.getPixels(p1, 0, width, 0, 0, width, height)
mask2.getPixels(p2, 0, width, 0, 0, width, height)
var sum = 0L
for (i in p1.indices) {
val v1 = if ((p1[i] ushr 24) > 0) 255 else 0
val v2 = if ((p2[i] ushr 24) > 0) 255 else 0
val diff = v1 - v2
sum += diff * diff
}
val dist = sqrt(sum.toDouble())
val max = sqrt((width * height).toDouble()) * 255.0
val allowed = max * (100 - thresholdPercent) / 100.0
return dist <= allowed
}
private fun calculateJaccardSimilarity(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean {
if (mask1.width != mask2.width || mask1.height != mask2.height) return false
val width = mask1.width
val height = mask1.height
val p1 = IntArray(width * height)
val p2 = IntArray(width * height)
mask1.getPixels(p1, 0, width, 0, 0, width, height)
mask2.getPixels(p2, 0, width, 0, 0, width, height)
var intersection = 0
var union = 0
for (i in p1.indices) {
val a = ((p1[i] ushr 24) and 0xFF) > 0
val b = ((p2[i] ushr 24) and 0xFF) > 0
if (a && b) intersection++
if (a || b) union++
}
if (union == 0) return false
val score = (intersection.toDouble() / union.toDouble()) * 100.0
return score >= thresholdPercent
}
fun centerCrop(bmp: Bitmap, tw: Int, th: Int): Bitmap {
val offsetX = (bmp.width - tw) / 2
val offsetY = (bmp.height - th) / 2
return Bitmap.createBitmap(bmp, offsetX, offsetY, tw, th)
}
fun scaleFitCenter(bmp: Bitmap, isFit: Boolean): Bitmap {
val w = bmp.width
val h = bmp.height
var scale: Float? = 0F
// scale to fit inside screen
if (isFit) {
scale = min(screenW.toFloat() / w, screenH.toFloat() / h)
} else {
scale = max(screenW.toFloat() / w, screenH.toFloat() / h)
}
val newW = (w * scale).toInt()
val newH = (h * scale).toInt()
return Bitmap.createScaledBitmap(bmp, newW, newH, true)
}
fun scaleBothToScreenAndCrop(
savedMask: Bitmap,
segmentedMask: Bitmap
): Pair<Bitmap, Bitmap> {
val scaledA = scaleFitCenter(savedMask, true)
val scaledB = scaleFitCenter(segmentedMask, false)
val wA = scaledA.width
val hA = scaledA.height
val wB = scaledB.width
val hB = scaledB.height
val targetW = min(wA, wB)
val targetH = min(hA, hB)
val croppedA = centerCrop(scaledA, targetW, targetH)
val croppedB = centerCrop(scaledB, targetW, targetH)
return Pair(croppedA, croppedB)
}
}

View File

@ -0,0 +1,144 @@
package com.example.livingai.camera
import android.Manifest
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import androidx.core.content.edit
import androidx.core.graphics.toColorInt
import com.example.livingai.R
import com.example.livingai.analysis.Analyzer
import com.example.livingai.analysis.FrameProcessor
import com.example.livingai.analysis.SavedMaskProcessor
import com.example.livingai.analysis.SegmentProcessor
import com.example.livingai.commons.Constants
import com.example.livingai.ui.page.HomeActivity
import com.google.android.material.button.MaterialButton
class CameraActivity : AppCompatActivity(), Analyzer.AnalysisListener {
private lateinit var overlayManager: OverlayManager
private lateinit var cameraProcessor: CameraProcessor
private lateinit var frameProcessor: FrameProcessor
private lateinit var segmentProcessor: SegmentProcessor
private lateinit var savedMaskProcessor: SavedMaskProcessor
private var cowName: String? = null
private var orientation: String? = null
private var savedMaskBitmap: Bitmap? = null
private var matchThreshold = 75
private var algorithm = HomeActivity.ALGORITHM_HAMMING
private var isAutoCapture = true
private var isMaskDisplayEnabled = false
private var isPhotoTaken = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cowName = intent.getStringExtra(Constants.COW_NAME)
orientation = intent.getStringExtra(Constants.ORIENTATION)
// Load settings
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
matchThreshold = prefs.getInt("THRESHOLD", 75)
algorithm = prefs.getString("ALGORITHM", HomeActivity.ALGORITHM_HAMMING) ?: HomeActivity.ALGORITHM_HAMMING
isAutoCapture = prefs.getBoolean(Constants.PREF_AUTO_CAPTURE, true)
isMaskDisplayEnabled = prefs.getBoolean(Constants.PREF_MASK_DISPLAY, false)
// Set orientation
if (orientation == "front" || orientation == "back") {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// Initialize Managers and Processors
overlayManager = OverlayManager(
findViewById(R.id.silhouetteOverlay),
findViewById(R.id.segmentationOverlay),
findViewById(R.id.savedMaskOverlay)
)
cameraProcessor = CameraProcessor(this, findViewById(R.id.cameraPreview), this)
frameProcessor = FrameProcessor()
segmentProcessor = SegmentProcessor()
savedMaskProcessor = SavedMaskProcessor(this)
setupUI()
loadMasks()
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
private fun setupUI() {
findViewById<MaterialButton>(R.id.btnExit).setOnClickListener { finish() }
val btnShutter = findViewById<MaterialButton>(R.id.btnShutter)
btnShutter.setOnClickListener {
cameraProcessor.takePhoto(
cowName, orientation,
intent.getIntExtra(Constants.SILHOUETTE_ID, 0),
intent.getStringExtra("RETAKE_IMAGE_PATH")
)
isPhotoTaken = true
}
val silhouetteId = intent.getIntExtra(Constants.SILHOUETTE_ID, 0)
overlayManager.setSilhouette(silhouetteId)
}
private fun loadMasks() {
savedMaskBitmap = savedMaskProcessor.loadSavedMask(orientation ?: "unknown")
overlayManager.showSavedMask(savedMaskBitmap, isMaskDisplayEnabled)
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) cameraProcessor.startCamera()
}
@ExperimentalGetImage
override fun onFrame(imageProxy: ImageProxy) {
if (isPhotoTaken) {
imageProxy.close()
return
}
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
val inputImage = frameProcessor.prepareInputImage(imageProxy, rotationDegrees)
if (inputImage != null) {
segmentProcessor.process(inputImage, savedMaskBitmap, matchThreshold, algorithm)
.addOnSuccessListener { result ->
runOnUiThread {
overlayManager.showSegmentationMask(result.mask, isMaskDisplayEnabled)
}
if (isAutoCapture && result.isMatch && !isPhotoTaken) {
isPhotoTaken = true
cameraProcessor.takePhoto(
cowName, orientation,
intent.getIntExtra(Constants.SILHOUETTE_ID, 0),
intent.getStringExtra("RETAKE_IMAGE_PATH")
)
}
}
.addOnFailureListener { e ->
Log.e("CameraActivity", "Frame processing error", e)
}
.addOnCompleteListener {
imageProxy.close()
}
} else {
imageProxy.close()
}
}
}

View File

@ -0,0 +1,177 @@
package com.example.livingai.camera
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.util.Log
import android.util.Size
import android.widget.Toast
import androidx.camera.core.AspectRatio
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.example.livingai.R
import com.example.livingai.analysis.Analyzer
import com.example.livingai.storage.StorageUtils
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.Executors
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
/**
* Responsible only for fetching camera frames and capturing images.
*/
class CameraProcessor(
private val context: Context,
private val previewView: PreviewView,
private val analysisListener: Analyzer.AnalysisListener
) {
private var imageCapture: ImageCapture? = null
private val cameraExecutor = Executors.newSingleThreadExecutor()
var analysisSize: Size? = null
private set
fun startCamera(onAnalysisSizeReady: (Int, Int) -> Unit = { _, _ -> }) {
val providerFuture = ProcessCameraProvider.getInstance(context)
providerFuture.addListener({
val cameraProvider = providerFuture.get()
val preview = androidx.camera.core.Preview.Builder().build()
preview.surfaceProvider = previewView.surfaceProvider
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
val metrics = context.resources.displayMetrics
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
val analyzer = ImageAnalysis.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setBackpressureStrategy(
ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
)
.build()
.also {
it.setAnalyzer(cameraExecutor, Analyzer(analysisListener))
}
cameraProvider.unbindAll()
if (context is LifecycleOwner) {
cameraProvider.bindToLifecycle(
context,
androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture,
analyzer
)
// Get analysis resolution
analyzer.resolutionInfo?.resolution?.let { size ->
analysisSize = size
onAnalysisSizeReady(size.width, size.height)
} ?: run {
// Fallback: if resolution info is not available immediately
}
}
}, ContextCompat.getMainExecutor(context))
}
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - 4.0 / 3.0) <= abs(previewRatio - 16.0 / 9.0)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
fun takePhoto(cowName: String?, orientation: String?, silhouetteId: Int, retakePath: String? = null) {
val imageCapture = imageCapture ?: return
val name = cowName ?: "unknown"
val side = orientation ?: "unknown"
// Find current count for this cow and orientation
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(cowFolder, filename)
val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e("CameraProcessor", "Photo capture failed: ${exc.message}", exc)
Toast.makeText(context, context.getString(R.string.toast_capture_failed), Toast.LENGTH_SHORT).show()
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
try {
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
val matrix = Matrix()
val rotation = if (orientation == "front" || orientation == "back") 90f else 0f
if (rotation != 0f) {
matrix.postRotate(rotation)
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
FileOutputStream(file).use { out ->
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
}
}
if (!retakePath.isNullOrEmpty()) {
val oldFile = File(retakePath)
if (oldFile.exists()) {
oldFile.delete()
}
}
val msg = "${context.getString(R.string.toast_saved_as)} $filename"
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
// Navigate to FullScreenImageActivity
val intent = android.content.Intent(context, com.example.livingai.ui.page.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)
context.startActivity(intent)
if (context is android.app.Activity) {
context.finish()
}
} catch (e: Exception) {
Log.e("CameraProcessor", "Error saving image", e)
Toast.makeText(context, context.getString(R.string.toast_error_saving_image), Toast.LENGTH_SHORT).show()
}
}
}
)
}
}

View File

@ -0,0 +1,33 @@
package com.example.livingai.camera
import android.graphics.Bitmap
import android.view.View
import android.widget.ImageView
import com.example.livingai.ui.overlay.MaskOverlay
import com.example.livingai.ui.overlay.SilhouetteOverlay
class OverlayManager(
private val silhouetteOverlay: SilhouetteOverlay,
private val segmentationOverlay: MaskOverlay,
private val savedMaskOverlay: MaskOverlay
) {
init {
// Configure scaling types
segmentationOverlay.scaleType = MaskOverlay.ScaleType.CENTER_CROP
savedMaskOverlay.scaleType = MaskOverlay.ScaleType.FIT_CENTER
}
fun setSilhouette(silhouetteId: Int) {
silhouetteOverlay.setSilhouette(silhouetteId)
}
fun showSegmentationMask(mask: Bitmap?, isEnabled: Boolean) {
segmentationOverlay.updateMask(if (isEnabled) mask else null)
}
fun showSavedMask(mask: Bitmap?, isEnabled: Boolean) {
savedMaskOverlay.updateMask(if (isEnabled) mask else null)
}
}

View File

@ -0,0 +1,19 @@
package com.example.livingai.commons
import android.provider.Settings.Global.getString
object Constants {
const val ALGORITHM_HAMMING = "Hamming Distance"
const val ALGORITHM_EUCLIDEAN = "Euclidean Distance"
const val ALGORITHM_JACCARD = "Jaccard Similarity"
const val PREF_AUTO_CAPTURE = "AUTO_CAPTURE_ENABLED"
const val PREF_MASK_DISPLAY = "MASK_DISPLAY_ENABLED"
const val PREF_DEBUG_ENABLE = "AUTO_DEBUG_ENABLED"
const val PREF_COW_ILLUSTRATION_INDEX = "COW_ILLUSTRATION_INDEX"
const val COW_NAME = "COW_NAME"
const val ORIENTATION = "ORIENTATION"
const val SILHOUETTE_ID = "SILHOUETTE_ID"
val ORIENTATION_NAMES = listOf("left", "right", "angle", "front", "back", "leftangle", "rightangle")
}

View File

@ -1,14 +1,13 @@
package com.example.animalrating
package com.example.livingai.storage
import android.os.Environment
import java.io.File
object StorageUtils {
private const val ROOT_FOLDER_NAME = "com.AnimalRating"
private const val ROOT_FOLDER_NAME = "com.LivingAI"
private fun getBaseFolder(): File {
// Changed to Android/media/com.AnimalRating as requested
val folder = File(Environment.getExternalStorageDirectory(), "Android/media/$ROOT_FOLDER_NAME")
if (!folder.exists()) {
folder.mkdirs()

View File

@ -0,0 +1,61 @@
package com.example.livingai.ui.overlay
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.util.AttributeSet
import android.view.View
import kotlin.math.max
import kotlin.math.min
class MaskOverlay(context: Context, attrs: AttributeSet?) : View(context, attrs) {
enum class ScaleType {
CENTER_CROP,
FIT_CENTER
}
private var maskBitmap: Bitmap? = null
private val matrix = Matrix()
var scaleType: ScaleType = ScaleType.CENTER_CROP
fun updateMask(bitmap: Bitmap?) {
maskBitmap = bitmap
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val bmp = maskBitmap ?: return
if (bmp.width == 0 || bmp.height == 0) return
val viewWidth = width.toFloat()
val viewHeight = height.toFloat()
val bmpWidth = bmp.width.toFloat()
val bmpHeight = bmp.height.toFloat()
matrix.reset()
val scaleX = viewWidth / bmpWidth
val scaleY = viewHeight / bmpHeight
val scale = if (scaleType == ScaleType.FIT_CENTER) {
min(scaleX, scaleY)
} else {
max(scaleX, scaleY)
}
val scaledWidth = bmpWidth * scale
val scaledHeight = bmpHeight * scale
val dx = (viewWidth - scaledWidth) / 2f
val dy = (viewHeight - scaledHeight) / 2f
matrix.postScale(scale, scale)
matrix.postTranslate(dx, dy)
canvas.drawBitmap(bmp, matrix, null)
}
}

View File

@ -1,4 +1,4 @@
package com.example.animalrating.ui
package com.example.livingai.ui.overlay
import android.content.Context
import android.graphics.*

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai.ui.page
import android.content.Intent
import android.content.pm.PackageManager
@ -19,6 +19,10 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.livingai.R
import com.example.livingai.camera.CameraActivity
import com.example.livingai.commons.Constants
import com.example.livingai.storage.StorageUtils
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import java.io.File
@ -28,21 +32,19 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class CowSelectionActivity : AppCompatActivity() {
class AddAnimalProfileActivity : AppCompatActivity() {
private var currentCowName: String? = null
private lateinit var imagesContainer: LinearLayout
private val storagePermissionCode = 101
private val orientationViews = mutableMapOf<String, View>()
private val initialImagePaths = mutableSetOf<String>()
private val orientationList = Constants.ORIENTATION_NAMES
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cow_selection)
StringProvider.initialize(this)
setupUIStrings()
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
@ -51,6 +53,8 @@ class CowSelectionActivity : AppCompatActivity() {
finish()
}
updateUILabels()
initializeDefaultMasks()
setupDropdowns()
@ -67,13 +71,9 @@ class CowSelectionActivity : AppCompatActivity() {
loadCowDetails(currentCowName!!)
}
orientationViews["left"] = findViewById(R.id.btnLeft)
orientationViews["right"] = findViewById(R.id.btnRight)
orientationViews["angle"] = findViewById(R.id.btnTop)
orientationViews["front"] = findViewById(R.id.btnFront)
orientationViews["back"] = findViewById(R.id.btnBack)
orientationViews["left_angle"] = findViewById(R.id.btnLeftAngle)
orientationViews["right_angle"] = findViewById(R.id.btnRightAngle)
for (orientation in orientationList) {
orientationViews[orientation] = findViewById(resources.getIdentifier(orientation, "id", packageName))
}
findViewById<Button>(R.id.btnNewCow).setOnClickListener {
if (checkStoragePermissions()) {
@ -82,13 +82,43 @@ class CowSelectionActivity : AppCompatActivity() {
requestStoragePermissions()
}
}
findViewById<Button>(R.id.btnCancel).setOnClickListener {
deleteSessionImages()
finish()
}
}
private fun updateUILabels() {
findViewById<TextView>(R.id.tvToolbarTitle).text = getString(R.string.title_cow_selection)
findViewById<TextView>(R.id.tvAddCowDetails).text = getString(R.string.title_add_cow_details)
findViewById<TextView>(R.id.tvLabelSpecies).text = getString(R.string.label_species)
findViewById<TextView>(R.id.tvLabelBreed).text = getString(R.string.label_breed)
findViewById<TextView>(R.id.tvLabelAge).text = getString(R.string.label_age)
findViewById<TextView>(R.id.tvLabelMilk).text = getString(R.string.label_milk_yield)
findViewById<TextView>(R.id.tvLabelCalving).text = getString(R.string.label_calving_no)
findViewById<TextView>(R.id.tvReproductiveStatus).text = getString(R.string.label_reproductive_status)
findViewById<RadioButton>(R.id.rbPregnant).text = getString(R.string.radio_pregnant)
findViewById<RadioButton>(R.id.rbCalved).text = getString(R.string.radio_calved)
findViewById<RadioButton>(R.id.rbNone).text = getString(R.string.radio_none)
findViewById<TextView>(R.id.tvLabelDescription).text = getString(R.string.hint_description)
findViewById<TextInputLayout>(R.id.tilDescription).hint = getString(R.string.hint_description)
findViewById<TextView>(R.id.tvUploadPhotos).text = getString(R.string.label_upload_photos)
findViewById<TextView>(R.id.tvFrontView).text = getString(R.string.text_front_view)
findViewById<TextView>(R.id.tvRearView).text = getString(R.string.text_rear_view)
findViewById<TextView>(R.id.tvLeftSide).text = getString(R.string.text_left_side)
findViewById<TextView>(R.id.tvRightSide).text = getString(R.string.text_right_side)
findViewById<TextView>(R.id.tvLeftAngle).text = getString(R.string.text_left_angle)
findViewById<TextView>(R.id.tvRightAngle).text = getString(R.string.text_right_angle)
findViewById<TextView>(R.id.tvAngleView).text = getString(R.string.text_angle_view)
findViewById<Button>(R.id.btnNewCow).text = getString(R.string.btn_save_profile)
findViewById<Button>(R.id.btnCancel).text = getString(R.string.btn_cancel)
}
private fun loadInitialImages() {
val name = currentCowName ?: return
val cowFolder = StorageUtils.getCowImageFolder(name)
@ -110,43 +140,13 @@ class CowSelectionActivity : AppCompatActivity() {
file.delete()
}
}
val remaining = cowFolder.listFiles()
if (remaining == null || remaining.isEmpty()) {
cowFolder.delete()
}
}
}
private fun setupUIStrings() {
findViewById<TextView>(R.id.tvToolbarTitle).text = StringProvider.getString("title_cow_selection")
findViewById<TextView>(R.id.tvAddCowDetails).text = StringProvider.getString("title_add_cow_details")
// Using hint_ keys for the new labels as they contain the appropriate text (e.g. "Species", "Breed")
findViewById<TextView>(R.id.tvLabelSpecies).text = StringProvider.getString("hint_species")
findViewById<TextView>(R.id.tvLabelBreed).text = StringProvider.getString("hint_breed")
findViewById<TextView>(R.id.tvLabelAge).text = StringProvider.getString("hint_age")
findViewById<TextView>(R.id.tvLabelMilk).text = StringProvider.getString("hint_milk_yield")
findViewById<TextView>(R.id.tvLabelCalving).text = StringProvider.getString("hint_calving_number")
findViewById<TextView>(R.id.tvLabelDescription).text = StringProvider.getString("hint_description")
findViewById<TextInputLayout>(R.id.tilSpecies).hint = null
findViewById<TextInputLayout>(R.id.tilBreed).hint = null
findViewById<TextInputLayout>(R.id.tilAge).hint = null
findViewById<TextInputLayout>(R.id.tilMilk).hint = null
findViewById<TextInputLayout>(R.id.tilCalving).hint = null
findViewById<TextInputLayout>(R.id.tilDescription).hint = null
findViewById<TextView>(R.id.tvReproductiveStatus).text = StringProvider.getString("label_reproductive_status")
findViewById<RadioButton>(R.id.rbPregnant).text = StringProvider.getString("radio_pregnant")
findViewById<RadioButton>(R.id.rbCalved).text = StringProvider.getString("radio_calved")
findViewById<RadioButton>(R.id.rbNone).text = StringProvider.getString("radio_none")
findViewById<TextView>(R.id.tvUploadPhotos).text = StringProvider.getString("label_upload_photos")
findViewById<Button>(R.id.btnNewCow).text = StringProvider.getString("btn_save_profile")
findViewById<Button>(R.id.btnCancel).text = StringProvider.getString("btn_cancel")
}
private fun checkStoragePermissions(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
@ -181,43 +181,39 @@ class CowSelectionActivity : AppCompatActivity() {
private fun saveProfile() {
val speciesDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).text.toString()
val breedDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerBreed).text.toString()
val speciesKey = StringProvider.getKeyForValue(speciesDisplay)
val species = if (speciesKey != null) StringProvider.getStringEnglish(speciesKey) else speciesDisplay
val breedKey = StringProvider.getKeyForValue(breedDisplay)
val breed = if (breedKey != null) StringProvider.getStringEnglish(breedKey) else breedDisplay
val species = speciesDisplay
val breed = breedDisplay
val ageInput = findViewById<TextInputEditText>(R.id.etAge).text.toString()
val milkInput = findViewById<TextInputLayout>(R.id.tilMilk).editText?.text.toString()
val calvingInput = findViewById<TextInputLayout>(R.id.tilCalving).editText?.text.toString()
val descriptionInput = findViewById<TextInputLayout>(R.id.tilDescription).editText?.text.toString()
val rgReproductive = findViewById<RadioGroup>(R.id.rgReproductiveStatus)
val reproductiveStatusKey = when(rgReproductive?.checkedRadioButtonId) {
R.id.rbPregnant -> "radio_pregnant"
R.id.rbCalved -> "radio_calved"
R.id.rbNone -> "radio_none"
else -> null
val reproductiveStatus = when(rgReproductive?.checkedRadioButtonId) {
R.id.rbPregnant -> getString(R.string.radio_pregnant)
R.id.rbCalved -> getString(R.string.radio_calved)
R.id.rbNone -> getString(R.string.radio_none)
else -> ""
}
val reproductiveStatus = if (reproductiveStatusKey != null) StringProvider.getStringEnglish(reproductiveStatusKey) else ""
val csvHeader = "CowID,Species,Breed,Age,MilkYield,CalvingNumber,ReproductiveStatus,Description\n"
val csvRow = "$currentCowName,$species,$breed,$ageInput,$milkInput,$calvingInput,$reproductiveStatus,$descriptionInput\n"
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
try {
val fileExists = csvFile.exists()
val lines = if (fileExists) csvFile.readLines().toMutableList() else mutableListOf()
if (!fileExists) {
lines.add(csvHeader.trim())
}
val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") }
if (existingIndex != -1) {
lines[existingIndex] = csvRow.trim()
} else {
@ -229,12 +225,12 @@ class CowSelectionActivity : AppCompatActivity() {
writer.write(line + "\n")
}
}
Toast.makeText(this, StringProvider.getString("toast_profile_saved"), Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.toast_profile_saved), Toast.LENGTH_SHORT).show()
finish()
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, StringProvider.getString("toast_error_saving_profile") + " ${e.message}", Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.toast_error_saving_profile) + " ${e.message}", Toast.LENGTH_SHORT).show()
}
}
@ -248,59 +244,55 @@ class CowSelectionActivity : AppCompatActivity() {
val record = lines.find { it.startsWith("$cowId,") }?.split(",") ?: return
if (record.size >= 8) {
val storedSpecies = record[1]
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else storedSpecies
val displaySpecies = record[1]
findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).setText(displaySpecies, false)
val storedBreed = record[2]
val breedKey = StringProvider.getKeyForEnglishValue(storedBreed)
val displayBreed = if (breedKey != null) StringProvider.getString(breedKey) else storedBreed
val displayBreed = record[2]
findViewById<AutoCompleteTextView>(R.id.spinnerBreed).setText(displayBreed, false)
findViewById<TextInputEditText>(R.id.etAge).setText(record[3])
findViewById<TextInputLayout>(R.id.tilMilk).editText?.setText(record[4])
findViewById<TextInputLayout>(R.id.tilCalving).editText?.setText(record[5])
val storedStatus = record[6]
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus)
when(statusKey) {
"radio_pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"radio_calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
"radio_none" -> findViewById<RadioButton>(R.id.rbNone).isChecked = true
else -> {
when (storedStatus) {
"Pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"Calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
"None" -> findViewById<RadioButton>(R.id.rbNone).isChecked = true
}
if (storedStatus == getString(R.string.radio_pregnant)) {
findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
} else if (storedStatus == getString(R.string.radio_calved)) {
findViewById<RadioButton>(R.id.rbCalved).isChecked = true
} else if (storedStatus == getString(R.string.radio_none)) {
findViewById<RadioButton>(R.id.rbNone).isChecked = true
} else {
when (storedStatus) {
"Pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"Calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
"None" -> findViewById<RadioButton>(R.id.rbNone).isChecked = true
}
}
findViewById<TextInputLayout>(R.id.tilDescription).editText?.setText(record[7])
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setupDropdowns() {
val species = listOf(
StringProvider.getString("species_cow"),
StringProvider.getString("species_buffalo")
getString(R.string.species_cow),
getString(R.string.species_buffalo)
)
val speciesAdapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, species)
findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).setAdapter(speciesAdapter)
val breeds = listOf(
StringProvider.getString("breed_holstein"),
StringProvider.getString("breed_jersey"),
StringProvider.getString("breed_sahiwal"),
StringProvider.getString("breed_gir"),
StringProvider.getString("breed_red_sindhi"),
StringProvider.getString("breed_murrah"),
StringProvider.getString("breed_surti")
getString(R.string.breed_holstein),
getString(R.string.breed_jersey),
getString(R.string.breed_sahiwal),
getString(R.string.breed_gir),
getString(R.string.breed_red_sindhi),
getString(R.string.breed_murrah),
getString(R.string.breed_surti)
)
val breedAdapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, breeds)
findViewById<AutoCompleteTextView>(R.id.spinnerBreed).setAdapter(breedAdapter)
@ -312,15 +304,10 @@ class CowSelectionActivity : AppCompatActivity() {
}
private fun initializeDefaultMasks() {
val orientationResources = mapOf(
"left" to R.drawable.left,
"right" to R.drawable.right,
"angle" to R.drawable.angle,
"front" to R.drawable.front,
"back" to R.drawable.back,
"leftangle" to R.drawable.leftangle,
"rightangle" to R.drawable.rightangle
)
val orientationResources = orientationList.associate { orientation ->
val resId = resources.getIdentifier(orientation, "drawable", packageName)
orientation to resId
}
orientationResources.forEach { (orientation, resId) ->
val filename = "${orientation}_mask.png"
@ -366,21 +353,17 @@ class CowSelectionActivity : AppCompatActivity() {
private fun refreshCowImages() {
val name = currentCowName ?: return
val orientations = mapOf(
"left" to Pair(R.drawable.left, R.id.btnLeft),
"right" to Pair(R.drawable.right, R.id.btnRight),
"angle" to Pair(R.drawable.angle, R.id.btnTop),
"front" to Pair(R.drawable.front, R.id.btnFront),
"back" to Pair(R.drawable.back, R.id.btnBack),
"leftangle" to Pair(R.drawable.leftangle, R.id.btnLeftAngle),
"rightangle" to Pair(R.drawable.rightangle, R.id.btnRightAngle)
)
val orientationResourcesPair = orientationList.associate { orientation ->
val resId = resources.getIdentifier(orientation, "drawable", packageName)
val btnId = resources.getIdentifier("btn${orientation}", "id", packageName)
orientation to Pair(resId, btnId)
}
val cowImagesFolder = StorageUtils.getCowImageFolder(name)
orientations.forEach { (orientation, pair) ->
orientationResourcesPair.forEach { (orientation, pair) ->
val (drawableId, viewId) = pair
val files = if (cowImagesFolder.exists()) {
cowImagesFolder.listFiles { _, fname -> fname.startsWith("${name}_${orientation}_") && fname.endsWith(".jpg") }
} else {
@ -391,18 +374,17 @@ class CowSelectionActivity : AppCompatActivity() {
val container = findViewById<LinearLayout>(viewId)
container.removeAllViews()
val key = when(orientation) {
"front" -> "text_front_view"
"back" -> "text_rear_view"
"left" -> "text_left_side"
"right" -> "text_right_side"
"angle" -> "text_angle_view"
"leftangle" -> "text_left_angle"
"rightangle" -> "text_right_angle"
else -> ""
val label = when(orientation) {
"front" -> getString(R.string.text_front_view)
"back" -> getString(R.string.text_rear_view)
"left" -> getString(R.string.text_left_side)
"right" -> getString(R.string.text_right_side)
"angle" -> getString(R.string.text_angle_view)
"leftangle" -> getString(R.string.text_left_angle)
"rightangle" -> getString(R.string.text_right_angle)
else -> orientation
}
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientation
if (latestFile != null && latestFile.exists()) {
val frameLayout = android.widget.FrameLayout(this)
@ -457,9 +439,9 @@ class CowSelectionActivity : AppCompatActivity() {
frameLayout.addView(imageView)
frameLayout.addView(labelView)
frameLayout.addView(deleteBtn)
container.addView(frameLayout)
imageView.setOnClickListener {
val intent = Intent(this, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", latestFile.absolutePath)
@ -469,10 +451,10 @@ class CowSelectionActivity : AppCompatActivity() {
intent.putExtra("SILHOUETTE_ID", drawableId)
startActivity(intent)
}
container.setOnClickListener(null)
container.isClickable = false
} else {
val iconView = ImageView(this)
val params = LinearLayout.LayoutParams(
@ -483,7 +465,7 @@ class CowSelectionActivity : AppCompatActivity() {
iconView.layoutParams = params
iconView.setImageResource(android.R.drawable.ic_menu_camera)
iconView.setColorFilter(Color.parseColor("#5D4037"))
val textView = TextView(this)
val textParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
@ -492,7 +474,7 @@ class CowSelectionActivity : AppCompatActivity() {
textParams.gravity = android.view.Gravity.CENTER_HORIZONTAL
textParams.topMargin = (4 * resources.displayMetrics.density).toInt()
textView.layoutParams = textParams
textView.text = label
textView.textSize = 12f
textView.setTextColor(Color.parseColor("#5D4037"))
@ -501,7 +483,7 @@ class CowSelectionActivity : AppCompatActivity() {
container.addView(textView)
container.setOnClickListener {
val intent = Intent(this, CameraProcessor::class.java)
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("SILHOUETTE_ID", drawableId)
intent.putExtra("COW_NAME", currentCowName)
intent.putExtra("ORIENTATION", orientation)

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai.ui.page
import android.content.Intent
import android.graphics.BitmapFactory
@ -8,6 +8,8 @@ import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.example.livingai.R
import com.example.livingai.camera.CameraActivity
import java.io.File
class FullScreenImageActivity : AppCompatActivity() {
@ -16,8 +18,6 @@ class FullScreenImageActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_full_screen_image)
StringProvider.initialize(this)
val imagePath = intent.getStringExtra("IMAGE_PATH")
val allowRetake = intent.getBooleanExtra("ALLOW_RETAKE", false)
val cowName = intent.getStringExtra("COW_NAME")
@ -35,11 +35,11 @@ class FullScreenImageActivity : AppCompatActivity() {
val btnRetake = findViewById<Button>(R.id.btnRetake)
if (allowRetake && imagePath != null) {
btnRetake.visibility = View.VISIBLE
btnRetake.text = "Retake Photo"
btnRetake.text = getString(R.string.btn_retake)
btnRetake.setOnClickListener {
// Launch camera to retake. Pass the current image path so CameraProcessor can overwrite/delete it on success.
val intent = Intent(this, CameraProcessor::class.java)
// Launch camera to retake. Pass the current image path so CameraActivity can overwrite/delete it on success.
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("SILHOUETTE_ID", silhouetteId)
intent.putExtra("COW_NAME", cowName)
intent.putExtra("ORIENTATION", orientation)
@ -52,11 +52,11 @@ class FullScreenImageActivity : AppCompatActivity() {
}
val btnBack = findViewById<ImageButton>(R.id.btnBack)
btnBack.contentDescription = StringProvider.getString("content_desc_back")
btnBack.contentDescription = getString(R.string.content_desc_back)
btnBack.setOnClickListener {
finish()
}
findViewById<ImageView>(R.id.fullScreenImageView).contentDescription = StringProvider.getString("content_desc_full_screen_image")
findViewById<ImageView>(R.id.fullScreenImageView).contentDescription = getString(R.string.content_desc_full_screen_image)
}
}
}

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai.ui.page
import android.content.Intent
import android.content.res.ColorStateList
@ -11,11 +11,13 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import com.example.livingai.R
import com.example.livingai.storage.StorageUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.io.File
import java.io.FileWriter
import java.util.Locale
class GalleryActivity : AppCompatActivity() {
@ -25,16 +27,12 @@ class GalleryActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery)
StringProvider.initialize(this)
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_gallery")
toolbar.setNavigationOnClickListener {
finish()
}
@ -42,7 +40,7 @@ class GalleryActivity : AppCompatActivity() {
container = findViewById(R.id.galleryContainer)
findViewById<FloatingActionButton>(R.id.fabAddCow).setOnClickListener {
val intent = Intent(this, CowSelectionActivity::class.java)
val intent = Intent(this, AddAnimalProfileActivity::class.java)
startActivity(intent)
}
@ -91,7 +89,7 @@ class GalleryActivity : AppCompatActivity() {
}
radius = 16 * resources.displayMetrics.density
cardElevation = 2 * resources.displayMetrics.density
setCardBackgroundColor(android.graphics.Color.WHITE)
setCardBackgroundColor(ContextCompat.getColor(context, R.color.surface_background))
}
// Horizontal Container (3:1 split)
@ -118,10 +116,10 @@ class GalleryActivity : AppCompatActivity() {
}
val nameView = TextView(this).apply {
text = if (details.isNotEmpty()) "${StringProvider.getString("text_cow_id")} $cowName" else cowName
text = if (details.isNotEmpty()) "${getString(R.string.text_cow_id)} $cowName" else cowName
textSize = 20f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.parseColor("#3E2723"))
setTextColor(ContextCompat.getColor(context, R.color.text_header_brown))
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
@ -133,38 +131,34 @@ class GalleryActivity : AppCompatActivity() {
if (details.size >= 7) {
// Translate values for display
val storedSpecies = details.getOrElse(1) { "-" }
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else storedSpecies
val storedBreed = details.getOrElse(2) { "-" }
val breedKey = StringProvider.getKeyForEnglishValue(storedBreed)
val displayBreed = if (breedKey != null) StringProvider.getString(breedKey) else storedBreed
val displaySpecies = details.getOrElse(1) { "-" }
val displayBreed = details.getOrElse(2) { "-" }
val displayStatus = details.getOrElse(6) { "-" }
val storedStatus = details.getOrElse(6) { "-" }
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus)
val displayStatus = if (statusKey != null) StringProvider.getString(statusKey) else storedStatus
val age = details.getOrElse(3) { "-" }
val milk = details.getOrElse(4) { "-" }
val calving = details.getOrElse(5) { "-" }
val infoText = StringBuilder()
infoText.append("${StringProvider.getString("label_species")} $displaySpecies ")
infoText.append("${StringProvider.getString("label_breed")} $displayBreed\n")
infoText.append("${StringProvider.getString("label_age")} ${details.getOrElse(3) { "-" }} ${StringProvider.getString("unit_years")} ")
infoText.append("${StringProvider.getString("label_milk_yield")} ${details.getOrElse(4) { "-" }} ${StringProvider.getString("unit_liters")}\n")
infoText.append("${StringProvider.getString("label_calving_no")} ${details.getOrElse(5) { "-" }} ")
infoText.append("${StringProvider.getString("label_status")} $displayStatus")
infoText.append("${getString(R.string.label_species)} $displaySpecies ")
infoText.append("${getString(R.string.label_breed)} $displayBreed\n")
infoText.append("${getString(R.string.label_age)} $age ${getString(R.string.unit_years)} ")
infoText.append("${getString(R.string.label_milk_yield)} $milk ${getString(R.string.unit_liters)}\n")
infoText.append("${getString(R.string.label_calving_no)} $calving ")
infoText.append("${getString(R.string.label_status)} $displayStatus")
val detailsView = TextView(this).apply {
text = infoText.toString()
textSize = 14f
setTextColor(android.graphics.Color.parseColor("#5D4037"))
setTextColor(ContextCompat.getColor(context, R.color.text_brown))
setLineSpacing(10f, 1f)
}
leftLayout.addView(detailsView)
} else {
val detailsView = TextView(this).apply {
text = "No details available"
text = getString(R.string.text_no_details)
textSize = 14f
setTextColor(android.graphics.Color.parseColor("#5D4037"))
setTextColor(ContextCompat.getColor(context, R.color.text_brown))
}
leftLayout.addView(detailsView)
}
@ -190,14 +184,14 @@ class GalleryActivity : AppCompatActivity() {
}
val editButton = MaterialButton(this).apply {
text = StringProvider.getString("btn_edit")
text = getString(R.string.btn_edit)
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.parseColor("#5D4037"))
setTextColor(ContextCompat.getColor(context, R.color.text_brown))
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.parseColor("#EFEBE9"))
backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.brown_light_background))
strokeWidth = (1 * resources.displayMetrics.density).toInt()
strokeColor = ColorStateList.valueOf(android.graphics.Color.parseColor("#5D4037"))
strokeColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.text_brown))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
@ -205,19 +199,19 @@ class GalleryActivity : AppCompatActivity() {
minimumHeight = 0
setOnClickListener {
val intent = Intent(this@GalleryActivity, CowSelectionActivity::class.java)
val intent = Intent(this@GalleryActivity, AddAnimalProfileActivity::class.java)
intent.putExtra("COW_NAME", cowName)
startActivity(intent)
}
}
val rateButton = MaterialButton(this).apply {
text = "Rate" // Add string if needed, skipping as per instruction to be concise unless requested
text = getString(R.string.btn_rate)
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.WHITE)
setTextColor(ContextCompat.getColor(context, R.color.white))
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.parseColor("#6D4C41"))
backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.earthy))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
@ -232,12 +226,12 @@ class GalleryActivity : AppCompatActivity() {
}
val deleteButton = MaterialButton(this).apply {
text = "Delete"
text = getString(R.string.btn_delete)
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.WHITE)
setTextColor(ContextCompat.getColor(context, R.color.white))
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.RED)
backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.shutter_red))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
@ -293,28 +287,21 @@ class GalleryActivity : AppCompatActivity() {
val parts = file.name.split("_")
var orientation = ""
if (parts.size >= 3) {
// format: cow_..._orientation_timestamp.jpg
// or cow_timestamp_orientation_...
// Let's guess. In CowSelectionActivity: ${name}_${orientation}_...
// name = cow_...
// so filename starts with name
// Let's just try to find a known orientation in the filename
val orientations = listOf("left", "right", "angle", "front", "back", "leftangle", "rightangle")
orientation = orientations.find { file.name.contains("_${it}_") } ?: ""
}
val key = when(orientation) {
"front" -> "text_front_view"
"back" -> "text_rear_view"
"left" -> "text_left_side"
"right" -> "text_right_side"
"angle" -> "text_angle_view"
"leftangle" -> "text_left_angle"
"rightangle" -> "text_right_angle"
else -> ""
val label = when(orientation) {
"front" -> getString(R.string.text_front_view)
"back" -> getString(R.string.text_rear_view)
"left" -> getString(R.string.text_left_side)
"right" -> getString(R.string.text_right_side)
"angle" -> getString(R.string.text_angle_view)
"leftangle" -> getString(R.string.text_left_angle)
"rightangle" -> getString(R.string.text_right_angle)
else -> orientation
}
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientation
labelView.text = label
@ -336,10 +323,10 @@ class GalleryActivity : AppCompatActivity() {
parentDir.delete()
}
}
Toast.makeText(this@GalleryActivity, StringProvider.getString("toast_image_deleted"), Toast.LENGTH_SHORT).show()
Toast.makeText(this@GalleryActivity, getString(R.string.toast_image_deleted), Toast.LENGTH_SHORT).show()
refreshGallery()
} else {
Toast.makeText(this@GalleryActivity, StringProvider.getString("toast_error_deleting_image"), Toast.LENGTH_SHORT).show()
Toast.makeText(this@GalleryActivity, getString(R.string.toast_error_deleting_image), Toast.LENGTH_SHORT).show()
}
}
@ -389,7 +376,7 @@ class GalleryActivity : AppCompatActivity() {
}
}
Toast.makeText(this, "Cow profile deleted", Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.toast_profile_deleted), Toast.LENGTH_SHORT).show()
refreshGallery()
}

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai.ui.page
import android.content.Intent
import android.content.pm.PackageManager
@ -21,20 +21,23 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.example.livingai.R
import com.example.livingai.commons.Constants
import com.google.android.material.button.MaterialButton
import com.google.android.material.switchmaterial.SwitchMaterial
import java.util.Locale
class HomeActivity : AppCompatActivity() {
companion object {
const val ALGORITHM_HAMMING = "Hamming Distance"
const val ALGORITHM_EUCLIDEAN = "Euclidean Distance"
const val ALGORITHM_JACCARD = "Jaccard Similarity"
const val ALGORITHM_HAMMING = Constants.ALGORITHM_HAMMING
const val ALGORITHM_EUCLIDEAN = Constants.ALGORITHM_EUCLIDEAN
const val ALGORITHM_JACCARD = Constants.ALGORITHM_JACCARD
private const val PERMISSION_REQUEST_CODE = 101
private const val PREF_COW_ILLUSTRATION_INDEX = "COW_ILLUSTRATION_INDEX"
const val PREF_AUTO_CAPTURE = "AUTO_CAPTURE_ENABLED"
const val PREF_DEBUG_ENABLE = "AUTO_DEBUG_ENABLED"
const val PREF_MASK_DISPLAY = "MASK_DISPLAY_ENABLED"
const val PREF_COW_ILLUSTRATION_INDEX = Constants.PREF_COW_ILLUSTRATION_INDEX
const val PREF_AUTO_CAPTURE = Constants.PREF_AUTO_CAPTURE
const val PREF_DEBUG_ENABLE = Constants.PREF_DEBUG_ENABLE
const val PREF_MASK_DISPLAY = Constants.PREF_MASK_DISPLAY
var IS_RELEASE_BUILD = true // Set to true for release
}
@ -44,8 +47,6 @@ class HomeActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
StringProvider.initialize(this)
setupUI()
checkAndRequestPermissions()
}
@ -97,7 +98,7 @@ class HomeActivity : AppCompatActivity() {
// Language Spinner
val languageSpinner = findViewById<Spinner>(R.id.spinnerLanguage)
val languages = StringProvider.getLanguages()
val languages = listOf("English", "Hindi")
val languageAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, languages)
languageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
languageSpinner.adapter = languageAdapter
@ -109,12 +110,10 @@ class HomeActivity : AppCompatActivity() {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedLanguage = languages[position]
val currentLang = prefs.getString("LANGUAGE", "English")
// Only update and recreate if language actually changed
if (selectedLanguage != currentLang) {
saveSettings() // Save UI state so it's not lost
StringProvider.setLanguage(selectedLanguage, this@HomeActivity)
// Post recreate to avoid WindowLeaked (allows spinner popup to close)
saveSettings()
setLanguage(selectedLanguage)
android.os.Handler(android.os.Looper.getMainLooper()).post {
recreate()
}
@ -123,17 +122,17 @@ class HomeActivity : AppCompatActivity() {
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
// Set text from StringProvider
findViewById<TextView>(R.id.tvTitle).text = StringProvider.getString("app_name")
findViewById<TextView>(R.id.tvSubtitle).text = StringProvider.getString("subtitle_home")
findViewById<MaterialButton>(R.id.btnViewGallery).text = StringProvider.getString("btn_view_gallery")
findViewById<MaterialButton>(R.id.btnSelectCow).text = StringProvider.getString("btn_select_cow")
findViewById<TextView>(R.id.tvAlgorithmLabel).text = StringProvider.getString("label_algorithm")
findViewById<TextView>(R.id.tvThresholdLabel).text = StringProvider.getString("label_match_threshold")
// Set text from R.string
findViewById<TextView>(R.id.tvTitle).text = getString(R.string.app_name)
findViewById<TextView>(R.id.tvSubtitle).text = getString(R.string.subtitle_home)
findViewById<MaterialButton>(R.id.btnViewGallery).text = getString(R.string.btn_view_gallery)
findViewById<MaterialButton>(R.id.btnSelectCow).text = getString(R.string.btn_select_cow)
findViewById<TextView>(R.id.tvAlgorithmLabel).text = getString(R.string.label_algorithm)
findViewById<TextView>(R.id.tvThresholdLabel).text = getString(R.string.label_match_threshold)
// Cow Illustration and Logic
val ivCowIllustration = findViewById<ImageView>(R.id.ivCowIllustration)
val savedIndex = prefs.getInt(PREF_COW_ILLUSTRATION_INDEX, 0)
val savedIndex = prefs.getInt(PREF_COW_ILLUSTRATION_INDEX, 4)
setCowIllustration(ivCowIllustration, savedIndex)
ivCowIllustration.setOnClickListener { view ->
@ -146,18 +145,15 @@ class HomeActivity : AppCompatActivity() {
}
findViewById<MaterialButton>(R.id.btnSelectCow).setOnClickListener {
// if (!IS_RELEASE_BUILD) { // This logic seemed wrong in previous thought, we want to start activity regardless?
// Ah, user said "dont show the settings panel". It doesn't mean disable "Add Cow".
// But previously I wrote logic to save settings before start.
if (!IS_RELEASE_BUILD) {
saveSettings()
}
startActivity(Intent(this, CowSelectionActivity::class.java))
startActivity(Intent(this, AddAnimalProfileActivity::class.java))
}
// Auto Capture Toggle
val switchAutoCapture = findViewById<SwitchMaterial>(R.id.switchAutoCapture)
switchAutoCapture.text = StringProvider.getString("label_auto_capture")
switchAutoCapture.text = getString(R.string.label_auto_capture)
val autoCaptureEnabled = prefs.getBoolean(PREF_AUTO_CAPTURE, false)
switchAutoCapture.isChecked = autoCaptureEnabled
@ -166,11 +162,11 @@ class HomeActivity : AppCompatActivity() {
intArrayOf(-android.R.attr.state_checked)
)
val thumbColors = intArrayOf(
Color.parseColor("#6D4C41"), // Dark Brown for On
ContextCompat.getColor(this, R.color.earthy), // Dark Brown for On
Color.GRAY // Grey for Off
)
val trackColors = intArrayOf(
Color.parseColor("#8D6E63"), // Lighter Brown for On track
ContextCompat.getColor(this, R.color.switch_track_brown), // Lighter Brown for On track
Color.LTGRAY // Light Grey for Off track
)
@ -182,7 +178,7 @@ class HomeActivity : AppCompatActivity() {
}
val switchEnableDebug = findViewById<SwitchMaterial>(R.id.switchEnableDebug)
switchEnableDebug.text = StringProvider.getString("label_enable_debug")
switchEnableDebug.text = getString(R.string.label_enable_debug)
val debugEnabled = prefs.getBoolean(PREF_DEBUG_ENABLE, false)
switchEnableDebug.isChecked = debugEnabled
IS_RELEASE_BUILD = !debugEnabled
@ -202,15 +198,15 @@ class HomeActivity : AppCompatActivity() {
}
val headerSettings = findViewById<TextView>(R.id.tvHeaderSettings)
if (headerSettings != null) headerSettings.text = StringProvider.getString("header_settings")
if (headerSettings != null) headerSettings.text = getString(R.string.header_settings)
// Algorithm Spinner
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
val displayAlgorithms = listOf(
StringProvider.getString("algo_hamming"),
StringProvider.getString("algo_euclidean"),
StringProvider.getString("algo_jaccard")
getString(R.string.algo_hamming),
getString(R.string.algo_euclidean),
getString(R.string.algo_jaccard)
)
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, displayAlgorithms)
@ -240,7 +236,7 @@ class HomeActivity : AppCompatActivity() {
// Mask Display Toggle
val switchMaskDisplay = findViewById<SwitchMaterial>(R.id.switchMaskDisplay)
switchMaskDisplay.text = StringProvider.getString("label_mask_display")
switchMaskDisplay.text = getString(R.string.label_mask_display)
val maskDisplayEnabled = prefs.getBoolean(PREF_MASK_DISPLAY, false)
switchMaskDisplay.isChecked = maskDisplayEnabled
@ -255,19 +251,29 @@ class HomeActivity : AppCompatActivity() {
if (IS_RELEASE_BUILD) {
val settingsView = findViewById<CardView>(R.id.viewSettings)
settingsView.visibility = View.GONE
// Force defaults if needed, but logic above already sets defaults from prefs or code constants.
// If user wants to FORCE defaults every time release build runs:
/*
prefs.edit().putString("ALGORITHM", ALGORITHM_JACCARD).apply()
prefs.edit().putInt("THRESHOLD", 88).apply()
prefs.edit().putBoolean(PREF_MASK_DISPLAY, false).apply()
*/
// Leaving as is based on previous logic.
} else {
findViewById<CardView>(R.id.viewSettings).visibility = View.VISIBLE
}
}
private fun setLanguage(language: String) {
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
prefs.edit().putString("LANGUAGE", language).apply()
val localeCode = when(language) {
"Hindi" -> "hi"
"Gujarati" -> "gu"
else -> "en"
}
val locale = Locale(localeCode)
Locale.setDefault(locale)
val config = resources.configuration
config.setLocale(locale)
createConfigurationContext(config)
resources.updateConfiguration(config, resources.displayMetrics)
}
private fun showIllustrationPopup(anchor: View, imageView: ImageView) {
val popup = PopupMenu(this, anchor)
for (i in 0..4) {
@ -318,7 +324,7 @@ class HomeActivity : AppCompatActivity() {
// Save to preferences
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
val showMask = prefs.getBoolean(PREF_MASK_DISPLAY, false)
val showMask = showSegmentationMask.isChecked
prefs.edit().apply {
putString("ALGORITHM", selectedAlgorithm)
putInt("THRESHOLD", threshold)
@ -331,6 +337,6 @@ class HomeActivity : AppCompatActivity() {
private fun saveSettingsAndStart() {
if(!IS_RELEASE_BUILD)
saveSettings()
startActivity(Intent(this, CowSelectionActivity::class.java))
startActivity(Intent(this, AddAnimalProfileActivity::class.java))
}
}
}

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai.ui.page
import android.content.Intent
import android.graphics.BitmapFactory
@ -12,6 +12,9 @@ import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import com.example.livingai.R
import com.example.livingai.storage.StorageUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import java.io.File
@ -40,8 +43,6 @@ class RatingActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rating)
StringProvider.initialize(this)
currentCowName = intent.getStringExtra("COW_NAME") ?: run {
finish()
return
@ -51,7 +52,7 @@ class RatingActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationOnClickListener { finish() }
findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_rate_cow")
findViewById<TextView>(R.id.tvToolbarTitle)?.text = getString(R.string.title_rate_cow)
setupUIStrings()
loadCowDetails()
@ -69,12 +70,12 @@ class RatingActivity : AppCompatActivity() {
}
private fun setupUIStrings() {
findViewById<TextView>(R.id.tvHeaderPhotos)?.text = StringProvider.getString("header_photos")
findViewById<TextView>(R.id.tvHeaderCowDetails)?.text = StringProvider.getString("header_cow_details")
findViewById<TextView>(R.id.tvHeaderFeatureRatings)?.text = StringProvider.getString("header_feature_ratings")
findViewById<TextInputLayout>(R.id.tilComments)?.hint = StringProvider.getString("hint_comments")
findViewById<MaterialButton>(R.id.btnSaveRating)?.text = StringProvider.getString("btn_save_rating")
findViewById<MaterialButton>(R.id.btnCancelRating)?.text = StringProvider.getString("btn_cancel")
findViewById<TextView>(R.id.tvHeaderPhotos)?.text = getString(R.string.header_photos)
findViewById<TextView>(R.id.tvHeaderCowDetails)?.text = getString(R.string.header_cow_details)
findViewById<TextView>(R.id.tvHeaderFeatureRatings)?.text = getString(R.string.header_feature_ratings)
findViewById<TextInputLayout>(R.id.tilComments)?.hint = getString(R.string.hint_comments)
findViewById<MaterialButton>(R.id.btnSaveRating)?.text = getString(R.string.btn_save_rating)
findViewById<MaterialButton>(R.id.btnCancelRating)?.text = getString(R.string.btn_cancel)
}
private fun loadCowDetails() {
@ -87,27 +88,18 @@ class RatingActivity : AppCompatActivity() {
val record = lines.find { it.startsWith("$currentCowName,") }?.split(",") ?: return
if (record.size >= 8) {
// Translate values for display
val storedSpecies = record[1]
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else storedSpecies
val storedBreed = record[2]
val breedKey = StringProvider.getKeyForEnglishValue(storedBreed)
val displayBreed = if (breedKey != null) StringProvider.getString(breedKey) else storedBreed
val storedStatus = record[6]
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus)
val displayStatus = if (statusKey != null) StringProvider.getString(statusKey) else storedStatus
val displaySpecies = record[1]
val displayBreed = record[2]
val displayStatus = record[6]
val infoText = StringBuilder()
infoText.append("${StringProvider.getString("label_species")} $displaySpecies\n")
infoText.append("${StringProvider.getString("label_breed")} $displayBreed\n")
infoText.append("${StringProvider.getString("label_age")} ${record[3]} ${StringProvider.getString("unit_years")}\n")
infoText.append("${StringProvider.getString("label_milk_yield")} ${record[4]} ${StringProvider.getString("unit_liters")}\n")
infoText.append("${StringProvider.getString("label_calving_no")} ${record[5]}\n")
infoText.append("${StringProvider.getString("label_status")} $displayStatus\n")
infoText.append("${StringProvider.getString("hint_description")}: ${record[7]}")
infoText.append("${getString(R.string.label_species)} $displaySpecies\n")
infoText.append("${getString(R.string.label_breed)} $displayBreed\n")
infoText.append("${getString(R.string.label_age)} ${record[3]} ${getString(R.string.unit_years)}\n")
infoText.append("${getString(R.string.label_milk_yield)} ${record[4]} ${getString(R.string.unit_liters)}\n")
infoText.append("${getString(R.string.label_calving_no)} ${record[5]}\n")
infoText.append("${getString(R.string.label_status)} $displayStatus\n")
infoText.append("${getString(R.string.hint_description)}: ${record[7]}")
findViewById<TextView>(R.id.tvCowDetails).text = infoText.toString()
}
@ -161,20 +153,20 @@ class RatingActivity : AppCompatActivity() {
ratingsContainer = findViewById(R.id.ratingsContainer)
ratingsContainer.removeAllViews()
val textColor = ContextCompat.getColor(this, R.color.text_brown)
features.forEach { feature ->
val featureView = layoutInflater.inflate(R.layout.item_feature_rating, ratingsContainer, false)
// Localize feature name
val key = "feature_" + feature.lowercase(Locale.ROOT).replace(" ", "_")
val displayName = StringProvider.getString(key)
val finalName = if (displayName.isNotEmpty()) displayName else feature
val resourceId = resources.getIdentifier(key, "string", packageName)
val displayName = if (resourceId != 0) getString(resourceId) else feature
featureView.findViewById<TextView>(R.id.tvFeatureName).text = finalName
featureView.findViewById<TextView>(R.id.tvFeatureName).text = displayName
val buttonContainer = featureView.findViewById<LinearLayout>(R.id.buttonContainer)
val segmentViews = mutableListOf<TextView>()
// Create 9 segments
for (i in 1..9) {
val tv = TextView(this)
val params = LinearLayout.LayoutParams(
@ -185,18 +177,16 @@ class RatingActivity : AppCompatActivity() {
tv.layoutParams = params
tv.text = i.toString()
tv.gravity = Gravity.CENTER
tv.setTextColor(ContextCompat.getColor(this, R.color.black))
tv.setTextColor(textColor)
tv.textSize = 14f
tv.setBackgroundColor(Color.TRANSPARENT)
tv.setOnClickListener {
val currentRating = ratingsMap[feature] ?: 0
if (currentRating == i) {
// Clicked already selected -> Clear selection
ratingsMap[feature] = 0
updateSegmentSelection(segmentViews, 0)
} else {
// Select new rating
ratingsMap[feature] = i
updateSegmentSelection(segmentViews, i)
}
@ -212,6 +202,9 @@ class RatingActivity : AppCompatActivity() {
}
private fun updateSegmentSelection(segments: List<TextView>, selectedRating: Int) {
val selectedColor = ContextCompat.getColor(this, R.color.earthy)
val textColorUnselected = ContextCompat.getColor(this, R.color.text_brown)
segments.forEachIndexed { index, tv ->
val rating = index + 1
if (rating == selectedRating) {
@ -219,18 +212,16 @@ class RatingActivity : AppCompatActivity() {
val radius = (8 * resources.displayMetrics.density)
val drawable = GradientDrawable()
drawable.setColor(Color.parseColor("#6D4C41"))
drawable.setColor(selectedColor)
if (rating == 1) {
drawable.cornerRadii = floatArrayOf(radius, radius, 0f, 0f, 0f, 0f, radius, radius)
} else if (rating == 9) {
drawable.cornerRadii = floatArrayOf(0f, 0f, radius, radius, radius, radius, 0f, 0f)
} else {
drawable.cornerRadius = 0f
when (rating) {
1 -> drawable.cornerRadii = floatArrayOf(radius, radius, 0f, 0f, 0f, 0f, radius, radius)
9 -> drawable.cornerRadii = floatArrayOf(0f, 0f, radius, radius, radius, radius, 0f, 0f)
else -> drawable.cornerRadius = 0f
}
tv.background = drawable
} else {
tv.setTextColor(Color.parseColor("#5D4037"))
tv.setTextColor(textColorUnselected)
tv.background = null
}
}
@ -243,16 +234,12 @@ class RatingActivity : AppCompatActivity() {
try {
val lines = ratingsFile.readLines()
// Format: CowID,Comments,Feature1,Feature2,...
val record = lines.find { it.startsWith("$currentCowName,") }?.split(",") ?: return
if (record.size >= 2) {
// Index 0: ID
// Index 1: Comments
val comments = record[1].replace(";", ",")
findViewById<TextInputLayout>(R.id.tilComments).editText?.setText(comments)
// Ratings start from index 2
features.forEachIndexed { index, feature ->
val ratingStr = record.getOrNull(index + 2)
val rating = ratingStr?.toIntOrNull() ?: 0
@ -260,7 +247,6 @@ class RatingActivity : AppCompatActivity() {
if (rating > 0) {
ratingsMap[feature] = rating
// Find view and update by index
val featureView = ratingsContainer.getChildAt(index)
if (featureView != null) {
@Suppress("UNCHECKED_CAST")
@ -282,16 +268,14 @@ class RatingActivity : AppCompatActivity() {
val docsFolder = StorageUtils.getDocumentsFolder()
val ratingsFile = File(docsFolder, "cow_ratings.csv")
// Dynamically build header
val headerBuilder = StringBuilder("CowID,Comments")
features.forEach { headerBuilder.append(",$it") }
val header = headerBuilder.toString()
// Build row
val rowBuilder = StringBuilder()
rowBuilder.append("$currentCowName,$comments")
features.forEach { feature ->
val rating = ratingsMap[feature] ?: 0 // Defaults to 0 if not selected
val rating = ratingsMap[feature] ?: 0
rowBuilder.append(",$rating")
}
val newRow = rowBuilder.toString()
@ -301,8 +285,6 @@ class RatingActivity : AppCompatActivity() {
if (lines.isEmpty()) {
lines.add(header)
} else if (lines[0] != header) {
// Header mismatch handling
}
val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") }
@ -319,10 +301,10 @@ class RatingActivity : AppCompatActivity() {
}
}
Toast.makeText(this, StringProvider.getString("toast_ratings_saved"), Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.toast_ratings_saved), Toast.LENGTH_SHORT).show()
finish()
} catch (e: Exception) {
Toast.makeText(this, StringProvider.getString("toast_error_saving_ratings"), Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.toast_error_saving_ratings), Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -1,4 +1,4 @@
package com.example.animalrating.ui.theme
package com.example.livingai.ui.theme
import androidx.compose.ui.graphics.Color

View File

@ -1,4 +1,4 @@
package com.example.animalrating.ui.theme
package com.example.livingai.ui.theme
import android.app.Activity
import android.os.Build
@ -34,7 +34,7 @@ private val LightColorScheme = lightColorScheme(
)
@Composable
fun AnimalRatingTheme(
fun LivingAITheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,

View File

@ -1,4 +1,4 @@
package com.example.animalrating.ui.theme
package com.example.livingai.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle

View File

@ -303,7 +303,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnFront"
android:id="@+id/btnfront"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -336,7 +336,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnBack"
android:id="@+id/btnback"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -369,7 +369,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnLeft"
android:id="@+id/btnleft"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -402,7 +402,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnRight"
android:id="@+id/btnright"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -435,7 +435,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnLeftAngle"
android:id="@+id/btnleftangle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -468,7 +468,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnRightAngle"
android:id="@+id/btnrightangle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -501,7 +501,7 @@
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnTop"
android:id="@+id/btnangle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -548,8 +548,9 @@
android:text="Save Profile"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/white"
app:cornerRadius="12dp"
app:backgroundTint="@color/text_tertiary"
app:backgroundTint="@color/earthy"
android:layout_marginEnd="8dp"/>
<com.google.android.material.button.MaterialButton

View File

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
android:background="@color/black">
<ImageView
android:id="@+id/fullScreenImageView"
@ -23,9 +23,10 @@
android:src="@drawable/ic_round_back_button"
android:background="@android:color/transparent"
android:scaleType="centerInside"
android:contentDescription="Back" />
android:contentDescription="Back"
app:tint="@color/white"/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/btnRetake"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -33,6 +34,9 @@
android:layout_centerHorizontal="true"
android:layout_marginBottom="32dp"
android:text="Retake"
android:visibility="gone" />
android:visibility="gone"
android:textColor="@color/white"
app:backgroundTint="@color/earthy"
app:cornerRadius="12dp"/>
</RelativeLayout>

View File

@ -48,7 +48,8 @@
app:boxStrokeWidth="0dp"
app:startIconDrawable="@android:drawable/ic_menu_search"
app:startIconTint="@color/text_secondary"
android:hint="Search profiles...">
android:hint="Search profiles..."
android:textColorHint="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@ -97,6 +98,6 @@
android:layout_margin="24dp"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"
app:backgroundTint="@color/text_tertiary"/>
app:backgroundTint="@color/earthy"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -33,7 +33,8 @@
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_sort_by_size"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Menu"
android:backgroundTint="@color/icon_tint_primary"
android:contentDescription="Settings"
app:tint="@color/icon_tint_primary"/>
</LinearLayout>
@ -93,11 +94,13 @@
android:text="Add New Cow Profile"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/white"
app:cornerRadius="16dp"
app:backgroundTint="@color/text_tertiary"
app:backgroundTint="@color/earthy"
app:icon="@android:drawable/ic_input_add"
app:iconGravity="textStart"
app:iconPadding="12dp"
app:iconTint="@color/white"
android:layout_marginBottom="16dp"
android:elevation="4dp"/>
@ -141,7 +144,7 @@
android:id="@+id/tvHeaderSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Settings"
android:text="Debug Configs"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
@ -161,12 +164,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_spinner_rounded"
android:backgroundTint="@color/input_background"
android:padding="4dp"
android:layout_marginBottom="20dp">
<Spinner
android:id="@+id/spinnerAlgorithm"
android:layout_width="match_parent"
android:layout_height="48dp"/>
android:layout_height="48dp"
android:backgroundTint="@color/text_primary"/>
</FrameLayout>
<!-- Threshold Slider -->
@ -200,8 +205,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progressTint="@color/text_tertiary"
android:thumbTint="@color/text_tertiary"
android:progressTint="@color/earthy"
android:thumbTint="@color/earthy"
android:layout_marginBottom="16dp"/>
<!-- Mask Display Toggle -->
@ -210,9 +215,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Show Segmentation Mask"
android:textColor="@color/text_secondary"
android:textColor="@color/text_primary"
android:textSize="14sp"
app:thumbTint="@color/text_tertiary"
app:thumbTint="@color/earthy"
app:trackTint="@color/input_background"/>
</LinearLayout>
@ -256,6 +261,7 @@
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/bg_spinner_rounded"
android:backgroundTint="@color/input_background"
android:layout_marginBottom="24dp"/>
<!-- Auto Capture Toggle -->
@ -264,9 +270,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Auto Capture"
android:textColor="@color/text_secondary"
android:textColor="@color/text_primary"
android:textSize="14sp"
app:thumbTint="@color/text_tertiary"
app:thumbTint="@color/earthy"
app:trackTint="@color/input_background"
android:layout_marginBottom="16dp"/>
@ -276,9 +282,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enable Debug"
android:textColor="@color/text_secondary"
android:textColor="@color/text_primary"
android:textSize="14sp"
app:thumbTint="@color/text_tertiary"
app:thumbTint="@color/earthy"
app:trackTint="@color/input_background"
android:layout_marginBottom="16dp"/>

View File

@ -8,28 +8,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.animalrating.ui.SilhouetteOverlay
<com.example.livingai.ui.overlay.SilhouetteOverlay
android:id="@+id/silhouetteOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- ImageView to display the saved mask (Green)-->
<ImageView
<com.example.livingai.ui.overlay.MaskOverlay
android:id="@+id/savedMaskOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:elevation="5dp"/>
<!-- ImageView to display the segmentation mask (Live)-->
<ImageView
<com.example.livingai.ui.overlay.MaskOverlay
android:id="@+id/segmentationOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:alpha="0.9"
android:elevation="10dp"/>
<com.example.livingai.ui.overlay.MaskOverlay
android:id="@+id/overlayBlue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="11dp"/>
<com.example.livingai.ui.overlay.MaskOverlay
android:id="@+id/overlayGreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="12dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnExit"
android:layout_width="48dp"
@ -39,30 +46,8 @@
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@drawable/ic_back_arrow"
app:iconSize="24dp"
app:iconTint="#FFFFFF"
app:backgroundTint="#80000000"
app:cornerRadius="24dp"
android:padding="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:gravity="center"/>
<!-- Toggle Auto/Manual Capture Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnToggleCaptureMode"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top|end"
android:layout_margin="16dp"
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@android:drawable/ic_menu_camera"
app:iconSize="24dp"
app:iconTint="#FFFFFF"
app:backgroundTint="#80000000"
app:iconTint="@color/white"
app:backgroundTint="@color/camera_overlay_button"
app:cornerRadius="24dp"
android:padding="0dp"
android:insetLeft="0dp"
@ -83,10 +68,10 @@
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@android:drawable/ic_menu_camera"
app:iconSize="40dp"
app:iconTint="#FFFFFF"
app:backgroundTint="#D32F2F"
app:iconTint="@color/white"
app:backgroundTint="@color/shutter_red"
app:cornerRadius="36dp"
app:strokeColor="#FFFFFF"
app:strokeColor="@color/white"
app:strokeWidth="4dp"
android:padding="0dp"
android:insetLeft="0dp"

View File

@ -98,7 +98,8 @@
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary"
android:hint="Comments">
android:hint="Comments"
android:textColorHint="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@ -133,8 +134,10 @@
android:text="Save Rating"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/white"
app:cornerRadius="12dp"
app:backgroundTint="@color/text_tertiary"/>
app:backgroundTint="@color/earthy"
android:layout_marginBottom="16dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelRating"
@ -143,8 +146,11 @@
android:text="Cancel"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/icon_tint_primary"
app:cornerRadius="12dp"
app:backgroundTint="@color/text_tertiary"/>
app:backgroundTint="@color/input_background"
app:strokeColor="@color/icon_tint_primary"
app:strokeWidth="1dp"/>
</LinearLayout>
</ScrollView>

View File

@ -20,6 +20,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_rating_segment"
android:backgroundTint="@color/input_background"
android:weightSum="9"
android:showDividers="middle"
android:divider="@drawable/divider_rating_segment"/>

View File

@ -16,7 +16,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:background="#E0E0E0"/>
android:background="@color/text_tertiary"/>
</androidx.cardview.widget.CardView>
@ -25,8 +25,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#80000000"
android:textColor="@android:color/white"
android:background="@color/camera_overlay_button"
android:textColor="@color/white"
android:textSize="10sp"
android:padding="2dp"
android:gravity="center"
@ -41,8 +41,8 @@
android:layout_margin="4dp"
app:icon="@android:drawable/ic_delete"
app:iconSize="14dp"
app:iconTint="#FFFFFF"
android:backgroundTint="#C62828"
app:iconTint="@color/white"
android:backgroundTint="@color/shutter_red"
app:cornerRadius="12dp"
android:padding="0dp"
android:insetLeft="0dp"

View File

@ -1,212 +0,0 @@
{
"English": {
"app_name": "Animal Rating",
"title_home": "Animal Rating",
"title_gallery": "Saved Cow Profiles",
"title_add_cow_details": "Add Cow Details",
"title_cow_selection": "Cow Selection",
"title_rate_cow": "Rate Cow",
"header_photos": "Photos",
"header_cow_details": "Cow Details",
"header_feature_ratings": "Feature Ratings",
"header_settings": "Settings",
"subtitle_home": "Record and manage cow information easily.",
"btn_view_gallery": "View Saved Cow Profiles",
"btn_select_cow": "Add New Cow Profile",
"label_algorithm": "Algorithm",
"label_match_threshold": "Match Threshold",
"label_auto_capture": "Auto Capture",
"label_enable_debug": "Enable Debug",
"label_mask_display": "Show Segmentation Mask",
"btn_edit": "Edit",
"btn_rate": "Rate",
"btn_delete": "Delete",
"btn_save_rating": "Save Rating",
"btn_retake": "Retake Photo",
"btn_exit": "Exit",
"hint_search_profiles": "Search profiles...",
"hint_comments": "Comments",
"toast_image_deleted": "Image deleted",
"toast_error_deleting_image": "Error deleting image",
"toast_ratings_saved": "Ratings Saved!",
"toast_error_saving_ratings": "Error saving ratings",
"toast_profile_deleted": "Cow profile deleted",
"text_cow_id": "Cow ID:",
"text_no_details": "No details available",
"label_species": "Species:",
"label_breed": "Breed:",
"label_age": "Age:",
"unit_years": "years",
"label_milk_yield": "Milk Yield:",
"unit_liters": "L",
"label_calving_no": "Calving #:",
"label_status": "Status:",
"hint_species": "Species",
"hint_breed": "Breed",
"hint_age": "Age (Years)",
"hint_milk_yield": "Milk Yield (L)",
"hint_calving_number": "Calving Number",
"label_reproductive_status": "Reproductive Status",
"radio_pregnant": "Pregnant",
"radio_calved": "Calved",
"radio_none": "None",
"hint_description": "Description",
"label_upload_photos": "Upload Photos",
"text_front_view": "Front View",
"text_rear_view": "Rear View",
"text_left_side": "Left Side",
"text_right_side": "Right Side",
"text_angle_view": "Angle View",
"text_left_angle": "Left Angle",
"text_right_angle": "Right Angle",
"btn_save_profile": "Save Profile",
"btn_cancel": "Cancel",
"toast_profile_saved": "Profile Saved!",
"toast_error_saving_profile": "Error saving profile:",
"toast_capture_failed": "Photo capture failed",
"toast_saved_as": "Saved as",
"toast_error_saving_image": "Error saving image",
"content_desc_back": "Back",
"content_desc_full_screen_image": "Full Screen Image",
"species_cow": "Cow",
"species_buffalo": "Buffalo",
"breed_holstein": "Holstein Friesian",
"breed_jersey": "Jersey",
"breed_sahiwal": "Sahiwal",
"breed_gir": "Gir",
"breed_red_sindhi": "Red Sindhi",
"breed_murrah": "Murrah",
"breed_surti": "Surti",
"feature_stature": "Stature",
"feature_chest_width": "Chest width",
"feature_body_depth": "Body depth",
"feature_angularity": "Angularity",
"feature_rump_angle": "Rump angle",
"feature_rump_width": "Rump width",
"feature_rear_legs_set": "Rear legs set",
"feature_rear_legs_rear_view": "Rear legs rear view",
"feature_foot_angle": "Foot angle",
"feature_fore_udder_attachment": "Fore udder attachment",
"feature_rear_udder_height": "Rear udder height",
"feature_central_ligament": "Central ligament",
"feature_udder_depth": "Udder depth",
"feature_front_teat_position": "Front teat position",
"feature_teat_length": "Teat length",
"feature_rear_teat_position": "Rear teat position",
"feature_locomotion": "Locomotion",
"feature_body_condition_score": "Body condition score",
"feature_hock_development": "Hock development",
"feature_bone_structure": "Bone structure",
"feature_rear_udder_width": "Rear udder width",
"feature_teat_thickness": "Teat thickness",
"feature_muscularity": "Muscularity",
"algo_hamming": "Hamming Distance",
"algo_euclidean": "Euclidean Distance",
"algo_jaccard": "Jaccard Similarity"
},
"Hindi": {
"app_name": "पशु रेटिंग",
"title_home": "पशु रेटिंग",
"title_gallery": "सहेजे गए गाय प्रोफाइल",
"title_add_cow_details": "गाय का विवरण जोड़ें",
"title_cow_selection": "गाय का चयन",
"title_rate_cow": "गाय का मूल्यांकन",
"header_photos": "तस्वीरें",
"header_cow_details": "गाय का विवरण",
"header_feature_ratings": "विशेषता रेटिंग",
"header_settings": "सेटिंग्स",
"subtitle_home": "गाय की जानकारी आसानी से रिकॉर्ड और प्रबंधित करें।",
"btn_view_gallery": "सहेजे गए प्रोफाइल देखें",
"btn_select_cow": "नई गाय प्रोफ़ाइल जोड़ें",
"label_algorithm": "एल्गोरिदम",
"label_match_threshold": "मैच थ्रेसहोल्ड",
"label_auto_capture": "ऑटो कैप्चर",
"label_enable_debug": "डिबग सक्षम करें",
"label_mask_display": "सेगमेंटेशन मास्क दिखाएं",
"btn_edit": "संपादित करें",
"btn_rate": "रेट करें",
"btn_delete": "हटाएँ",
"btn_save_rating": "रेटिंग सहेजें",
"btn_retake": "फोटो फिर से लें",
"btn_exit": "बाहर जाएं",
"hint_search_profiles": "प्रोफ़ाइल खोजें...",
"hint_comments": "टिप्पणियाँ",
"toast_image_deleted": "छवि हटा दी गई",
"toast_error_deleting_image": "छवि हटाने में त्रुटि",
"toast_ratings_saved": "रेटिंग सहेजी गई!",
"toast_error_saving_ratings": "रेटिंग सहेजने में त्रुटि",
"toast_profile_deleted": "गाय प्रोफ़ाइल हटाई गई",
"text_cow_id": "गाय आईडी:",
"text_no_details": "कोई विवरण उपलब्ध नहीं",
"label_species": "प्रजाति:",
"label_breed": "नस्ल:",
"label_age": "आयु:",
"unit_years": "वर्ष",
"label_milk_yield": "दूध की उपज:",
"unit_liters": "L",
"label_calving_no": "बछड़ों की संख्या:",
"label_status": "स्थिति:",
"hint_species": "प्रजाति",
"hint_breed": "नस्ल",
"hint_age": "आयु (वर्ष)",
"hint_milk_yield": "दूध की उपज (L)",
"hint_calving_number": "बछड़ों की संख्या",
"label_reproductive_status": "प्रजनन स्थिति",
"radio_pregnant": "गर्भवती",
"radio_calved": "बछड़ा हुआ",
"radio_none": "कोई नहीं",
"hint_description": "विवरण",
"label_upload_photos": "तस्वीरें अपलोड करें",
"text_front_view": "सामने का दृश्य",
"text_rear_view": "पीछे का दृश्य",
"text_left_side": "बाईं ओर",
"text_right_side": "दाईं ओर",
"text_angle_view": "कोणीय दृश्य",
"text_left_angle": "बायां कोण",
"text_right_angle": "दायां कोण",
"btn_save_profile": "प्रोफ़ाइल सहेजें",
"btn_cancel": "रद्द करें",
"toast_profile_saved": "प्रोफ़ाइल सहेजा गया!",
"toast_error_saving_profile": "प्रोफ़ाइल सहेजने में त्रुटि:",
"toast_capture_failed": "फोटो कैप्चर विफल",
"toast_saved_as": "के रूप में सहेजा गया",
"toast_error_saving_image": "छवि सहेजने में त्रुटि",
"content_desc_back": "वापस",
"content_desc_full_screen_image": "पूर्ण स्क्रीन छवि",
"species_cow": "गाय",
"species_buffalo": "भैंस",
"breed_holstein": "होल्सटीन फ्राइज़ियन",
"breed_jersey": "जर्सी",
"breed_sahiwal": "साहिवाल",
"breed_gir": "गिर",
"breed_red_sindhi": "लाल सिंधी",
"breed_murrah": "मुर्रा",
"breed_surti": "सुरती",
"feature_stature": "कद",
"feature_chest_width": "छाती की चौड़ाई",
"feature_body_depth": "शरीर की गहराई",
"feature_angularity": "कोणीयता",
"feature_rump_angle": "रंप कोण",
"feature_rump_width": "रंप चौड़ाई",
"feature_rear_legs_set": "पिछले पैर सेट",
"feature_rear_legs_rear_view": "पिछले पैर पीछे का दृश्य",
"feature_foot_angle": "पैर का कोण",
"feature_fore_udder_attachment": "आगे का अडर जुड़ाव",
"feature_rear_udder_height": "पीछे के अडर की ऊंचाई",
"feature_central_ligament": "केंद्रीय लिगामेंट",
"feature_udder_depth": "अडर की गहराई",
"feature_front_teat_position": "सामने के थन की स्थिति",
"feature_teat_length": "थन की लंबाई",
"feature_rear_teat_position": "पीछे के थन की स्थिति",
"feature_locomotion": "चाल",
"feature_body_condition_score": "शारीरिक स्थिति स्कोर",
"feature_hock_development": "हॉक विकास",
"feature_bone_structure": "हड्डी की संरचना",
"feature_rear_udder_width": "पीछे के अडर की चौड़ाई",
"feature_teat_thickness": "थन की मोटाई",
"feature_muscularity": "मांसलता",
"algo_hamming": "हैमिंग दूरी",
"algo_euclidean": "यूक्लिडियन दूरी",
"algo_jaccard": "जैकार्ड समानता"
}
}

View File

@ -0,0 +1,127 @@
<resources>
<string name="app_name">पशु रेटिंग</string>
<string name="title_home">पशु रेटिंग</string>
<string name="title_gallery">सहेजे गए गाय प्रोफाइल</string>
<string name="title_add_cow_details">गाय का विवरण जोड़ें</string>
<string name="title_cow_selection">गाय का चयन</string>
<string name="title_rate_cow">गाय का मूल्यांकन</string>
<string name="header_photos">तस्वीरें</string>
<string name="header_cow_details">गाय का विवरण</string>
<string name="header_feature_ratings">विशेषता रेटिंग</string>
<string name="header_settings">सेटिंग्स</string>
<string name="subtitle_home">गाय की जानकारी आसानी से रिकॉर्ड और प्रबंधित करें।</string>
<string name="btn_view_gallery">सहेजे गए प्रोफाइल देखें</string>
<string name="btn_select_cow">नई गाय प्रोफ़ाइल जोड़ें</string>
<string name="label_algorithm">एल्गोरिदम</string>
<string name="label_match_threshold">मैच थ्रेसहोल्ड</string>
<string name="label_auto_capture">ऑटो कैप्चर</string>
<string name="label_enable_debug">डिबग सक्षम करें</string>
<string name="label_mask_display">सेगमेंटेशन मास्क दिखाएं</string>
<string name="btn_edit">संपादित करें</string>
<string name="btn_rate">रेट करें</string>
<string name="btn_delete">हटाएँ</string>
<string name="btn_save_rating">रेटिंग सहेजें</string>
<string name="btn_retake">फोटो फिर से लें</string>
<string name="btn_exit">बाहर जाएं</string>
<string name="hint_search_profiles">प्रोफ़ाइल खोजें&#8230;</string>
<string name="hint_comments">टिप्पणियाँ</string>
<string name="toast_image_deleted">छवि हटा दी गई</string>
<string name="toast_error_deleting_image">छवि हटाने में त्रुटि</string>
<string name="toast_ratings_saved">रेटिंग सहेजी गई!</string>
<string name="toast_error_saving_ratings">रेटिंग सहेजने में त्रुटि</string>
<string name="toast_profile_deleted">गाय प्रोफ़ाइल हटाई गई</string>
<string name="text_cow_id">गाय आईडी:</string>
<string name="text_no_details">कोई विवरण उपलब्ध नहीं</string>
<string name="label_species">प्रजाति:</string>
<string name="label_breed">नस्ल:</string>
<string name="label_age">आयु:</string>
<string name="unit_years">वर्ष</string>
<string name="label_milk_yield">दूध की उपज:</string>
<string name="unit_liters">L</string>
<string name="label_calving_no">बछड़ों की संख्या:</string>
<string name="label_status">स्थिति:</string>
<string name="hint_species">प्रजाति</string>
<string name="hint_breed">नस्ल</string>
<string name="hint_age">आयु (वर्ष)</string>
<string name="hint_milk_yield">दूध की उपज (L)</string>
<string name="hint_calving_number">बछड़ों की संख्या</string>
<string name="label_reproductive_status">प्रजनन स्थिति</string>
<string name="radio_pregnant">गर्भवती</string>
<string name="radio_calved">बछड़ा हुआ</string>
<string name="radio_none">कोई नहीं</string>
<string name="hint_description">विवरण</string>
<string name="label_upload_photos">तस्वीरें अपलोड करें</string>
<string name="text_front_view">सामने का दृश्य</string>
<string name="text_rear_view">पीछे का दृश्य</string>
<string name="text_left_side">बाईं ओर</string>
<string name="text_right_side">दाईं ओर</string>
<string name="text_angle_view">कोणीय दृश्य</string>
<string name="text_left_angle">बायां कोण</string>
<string name="text_right_angle">दायां कोण</string>
<string name="btn_save_profile">प्रोफ़ाइल सहेजें</string>
<string name="btn_cancel">रद्द करें</string>
<string name="toast_profile_saved">प्रोफ़ाइल सहेजा गया!</string>
<string name="toast_error_saving_profile">प्रोफ़ाइल सहेजने में त्रुटि:</string>
<string name="toast_capture_failed">फोटो कैप्चर विफल</string>
<string name="toast_saved_as">के रूप में सहेजा गया</string>
<string name="toast_error_saving_image">छवि सहेजने में त्रुटि</string>
<string name="content_desc_back">वापस</string>
<string name="content_desc_full_screen_image">पूर्ण स्क्रीन छवि</string>
<string name="species_cow">गाय</string>
<string name="species_buffalo">भैंस</string>
<string name="breed_holstein">होल्सटीन फ्राइज़ियन</string>
<string name="breed_jersey">जर्सी</string>
<string name="breed_sahiwal">साहिवाल</string>
<string name="breed_gir">गिर</string>
<string name="breed_red_sindhi">लाल सिंधी</string>
<string name="breed_murrah">मुर्रा</string>
<string name="breed_surti">सुरती</string>
<string name="feature_stature">कद</string>
<string name="feature_chest_width">छाती की चौड़ाई</string>
<string name="feature_body_depth">शरीर की गहराई</string>
<string name="feature_angularity">कोणीयता</string>
<string name="feature_rump_angle">रंप कोण</string>
<string name="feature_rump_width">रंप चौड़ाई</string>
<string name="feature_rear_legs_set">पिछले पैर सेट</string>
<string name="feature_rear_legs_rear_view">पिछले पैर पीछे का दृश्य</string>
<string name="feature_foot_angle">पैर का कोण</string>
<string name="feature_fore_udder_attachment">आगे का अडर जुड़ाव</string>
<string name="feature_rear_udder_height">पीछे के अडर की ऊंचाई</string>
<string name="feature_central_ligament">केंद्रीय लिगामेंट</string>
<string name="feature_udder_depth">अडर की गहराई</string>
<string name="feature_front_teat_position">सामने के थन की स्थिति</string>
<string name="feature_teat_length">थन की लंबाई</string>
<string name="feature_rear_teat_position">पीछे के थन की स्थिति</string>
<string name="feature_locomotion">चाल</string>
<string name="feature_body_condition_score">शारीरिक स्थिति स्कोर</string>
<string name="feature_hock_development">हॉक विकास</string>
<string name="feature_bone_structure">हड्डी की संरचना</string>
<string name="feature_rear_udder_width">पीछे के अडर की चौड़ाई</string>
<string name="feature_teat_thickness">थन की मोटाई</string>
<string name="feature_muscularity">मांसलता</string>
<string name="algo_hamming">हैमिंग दूरी</string>
<string name="algo_euclidean">यूक्लिडियन दूरी</string>
<string name="algo_jaccard">जैकार्ड समानता</string>
</resources>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Semantic Colors for AnimalRating (Dark Mode) -->
<color name="text_primary">#EFEBE9</color>
<color name="text_secondary">#BCAAA4</color>
<color name="text_tertiary">#A1887F</color>
<color name="page_background">#121212</color>
<color name="surface_background">#1E1E1E</color>
<color name="input_background">#2C2C2C</color>
<color name="icon_tint_primary">#BCAAA4</color>
</resources>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AnimalRating" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">#A1887F</item>
<item name="colorPrimaryVariant">#6D4C41</item>
<item name="colorOnPrimary">#000000</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#FF8A65</item>
<item name="colorSecondaryVariant">#FF8A65</item>
<item name="colorOnSecondary">#000000</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -5,16 +5,24 @@
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="earthy">#6D4C41</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Semantic Colors for AnimalRating -->
<color name="text_primary">#3E2723</color>
<color name="text_secondary">#5D4037</color>
<color name="text_tertiary">#6D4C41</color>
<color name="page_background">#F7F7F7</color>
<color name="surface_background">#FFFFFF</color>
<color name="page_background">#FFFFFF</color>
<color name="text_primary">#212121</color>
<color name="text_secondary">#757575</color>
<color name="text_tertiary">#BDBDBD</color>
<color name="input_background">#F5F5F5</color>
<color name="icon_tint_primary">#5D4037</color>
<color name="placeholder_background">#E0E0E0</color>
<color name="icon_tint_primary">#212121</color>
<color name="camera_overlay_button">#80000000</color>
<color name="shutter_red">#D32F2F</color>
<color name="text_brown">#5D4037</color>
<color name="text_header_brown">#3E2723</color>
<color name="brown_light_background">#EFEBE9</color>
<color name="switch_track_brown">#8D6E63</color>
<color name="translucent_white">#80FFFFFF</color>
</resources>

View File

@ -1,3 +1,127 @@
<resources>
<string name="app_name">AnimalRating</string>
</resources>
<string name="app_name">LivingAI</string>
<string name="title_home">LivingAI</string>
<string name="title_gallery">Saved Cow Profiles</string>
<string name="title_add_cow_details">Add Cow Details</string>
<string name="title_cow_selection">Cow Selection</string>
<string name="title_rate_cow">Rate Cow</string>
<string name="header_photos">Photos</string>
<string name="header_cow_details">Cow Details</string>
<string name="header_feature_ratings">Feature Ratings</string>
<string name="header_settings">Debug Configs</string>
<string name="subtitle_home">Record and manage cow information easily.</string>
<string name="btn_view_gallery">View Saved Cow Profiles</string>
<string name="btn_select_cow">Add New Cow Profile</string>
<string name="label_algorithm">Algorithm</string>
<string name="label_match_threshold">Match Threshold</string>
<string name="label_auto_capture">Auto Capture</string>
<string name="label_enable_debug">Enable Debug</string>
<string name="label_mask_display">Show Segmentation Mask</string>
<string name="btn_edit">Edit</string>
<string name="btn_rate">Rate</string>
<string name="btn_delete">Delete</string>
<string name="btn_save_rating">Save Rating</string>
<string name="btn_retake">Retake Photo</string>
<string name="btn_exit">Exit</string>
<string name="hint_search_profiles">Search profiles...</string>
<string name="hint_comments">Comments</string>
<string name="toast_image_deleted">Image deleted</string>
<string name="toast_error_deleting_image">Error deleting image</string>
<string name="toast_ratings_saved">Ratings Saved!</string>
<string name="toast_error_saving_ratings">Error saving ratings</string>
<string name="toast_profile_deleted">Cow profile deleted</string>
<string name="text_cow_id">Cow ID:</string>
<string name="text_no_details">No details available</string>
<string name="label_species">Species:</string>
<string name="label_breed">Breed:</string>
<string name="label_age">Age:</string>
<string name="unit_years">years</string>
<string name="label_milk_yield">Milk Yield:</string>
<string name="unit_liters">L</string>
<string name="label_calving_no">Calving #:</string>
<string name="label_status">Status:</string>
<string name="hint_species">Species</string>
<string name="hint_breed">Breed</string>
<string name="hint_age">Age (Years)</string>
<string name="hint_milk_yield">Milk Yield (L)</string>
<string name="hint_calving_number">Calving Number</string>
<string name="label_reproductive_status">Reproductive Status</string>
<string name="radio_pregnant">Pregnant</string>
<string name="radio_calved">Calved</string>
<string name="radio_none">None</string>
<string name="hint_description">Description</string>
<string name="label_upload_photos">Upload Photos</string>
<string name="text_front_view">Front View</string>
<string name="text_rear_view">Rear View</string>
<string name="text_left_side">Left Side</string>
<string name="text_right_side">Right Side</string>
<string name="text_angle_view">Angle View</string>
<string name="text_left_angle">Left Angle</string>
<string name="text_right_angle">Right Angle</string>
<string name="btn_save_profile">Save Profile</string>
<string name="btn_cancel">Cancel</string>
<string name="toast_profile_saved">Profile Saved!</string>
<string name="toast_error_saving_profile">Error saving profile:</string>
<string name="toast_capture_failed">Photo capture failed</string>
<string name="toast_saved_as">Saved as</string>
<string name="toast_error_saving_image">Error saving image</string>
<string name="content_desc_back">Back</string>
<string name="content_desc_full_screen_image">Full Screen Image</string>
<string name="species_cow">Cow</string>
<string name="species_buffalo">Buffalo</string>
<string name="breed_holstein">Holstein Friesian</string>
<string name="breed_jersey">Jersey</string>
<string name="breed_sahiwal">Sahiwal</string>
<string name="breed_gir">Gir</string>
<string name="breed_red_sindhi">Red Sindhi</string>
<string name="breed_murrah">Murrah</string>
<string name="breed_surti">Surti</string>
<string name="feature_stature">Stature</string>
<string name="feature_chest_width">Chest width</string>
<string name="feature_body_depth">Body depth</string>
<string name="feature_angularity">Angularity</string>
<string name="feature_rump_angle">Rump angle</string>
<string name="feature_rump_width">Rump width</string>
<string name="feature_rear_legs_set">Rear legs set</string>
<string name="feature_rear_legs_rear_view">Rear legs rear view</string>
<string name="feature_foot_angle">Foot angle</string>
<string name="feature_fore_udder_attachment">Fore udder attachment</string>
<string name="feature_rear_udder_height">Rear udder height</string>
<string name="feature_central_ligament">Central ligament</string>
<string name="feature_udder_depth">Udder depth</string>
<string name="feature_front_teat_position">Front teat position</string>
<string name="feature_teat_length">Teat length</string>
<string name="feature_rear_teat_position">Rear teat position</string>
<string name="feature_locomotion">Locomotion</string>
<string name="feature_body_condition_score">Body condition score</string>
<string name="feature_hock_development">Hock development</string>
<string name="feature_bone_structure">Bone structure</string>
<string name="feature_rear_udder_width">Rear udder width</string>
<string name="feature_teat_thickness">Teat thickness</string>
<string name="feature_muscularity">Muscularity</string>
<string name="algo_hamming">Hamming Distance</string>
<string name="algo_euclidean">Euclidean Distance</string>
<string name="algo_jaccard">Jaccard Similarity</string>
</resources>

View File

@ -1,17 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AnimalRating" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">#6D4C41</item>
<item name="colorPrimaryVariant">#5D4037</item>
<item name="colorOnPrimary">#FFFFFF</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">#FF5722</item>
<item name="colorSecondaryVariant">#E64A19</item>
<item name="colorOnSecondary">#FFFFFF</item>
<!-- Status bar color. -->
<style name="Theme.LivingAI" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,4 +1,4 @@
package com.example.animalrating
package com.example.livingai
import org.junit.Test

View File

@ -8,6 +8,8 @@ espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.12.0"
composeBom = "2024.09.00"
appcompat = "1.7.0"
material = "1.12.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -24,9 +26,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@ -1,4 +1,4 @@
#Sun Nov 23 20:12:35 IST 2025
#Sat Nov 29 09:53:57 IST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip

View File

@ -19,6 +19,6 @@ dependencyResolutionManagement {
}
}
rootProject.name = "AnimalRating"
rootProject.name = "LivingAI"
include(":app")

View File

@ -1 +0,0 @@
Adobe Stock - https://stock.adobe.com/