Initial commit

This commit is contained in:
SaiD 2025-11-30 12:33:44 +05:30
commit f77eef736c
93 changed files with 5360 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

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

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

13
.idea/deviceManager.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

19
.idea/gradle.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

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>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

9
.idea/misc.xml Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/studiobot.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

74
app/build.gradle.kts Normal file
View File

@ -0,0 +1,74 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.livingai"
compileSdk = 36
defaultConfig {
applicationId = "com.example.livingai"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
viewBinding = true
}
}
dependencies {
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("com.google.mlkit:object-detection:17.0.2")
implementation("com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
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)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.example.livingai
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.livingai", appContext.packageName)
}
}

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<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" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LivingAI">
<activity
android:name=".ui.page.HomeActivity"
android:exported="true"
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=".ui.page.GalleryActivity"
android:exported="false"
android:label="@string/title_gallery"
android:theme="@style/Theme.LivingAI" />
<activity
android:name=".ui.page.AddAnimalProfileActivity"
android:exported="false"
android:label="@string/title_add_cow_details"
android:theme="@style/Theme.LivingAI"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.page.RatingActivity"
android:exported="false"
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

@ -0,0 +1,17 @@
package com.example.livingai.analysis
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
class Analyzer(
private val listener: AnalysisListener
) : ImageAnalysis.Analyzer {
interface AnalysisListener {
fun onFrame(imageProxy: ImageProxy)
}
override fun analyze(image: ImageProxy) {
listener.onFrame(image)
}
}

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

@ -0,0 +1,45 @@
package com.example.livingai.storage
import android.os.Environment
import java.io.File
object StorageUtils {
private const val ROOT_FOLDER_NAME = "com.LivingAI"
private fun getBaseFolder(): File {
val folder = File(Environment.getExternalStorageDirectory(), "Android/media/$ROOT_FOLDER_NAME")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
fun getDocumentsFolder(): File {
val folder = File(getBaseFolder(), "Documents")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
fun getImagesBaseFolder(): File {
val folder = File(getBaseFolder(), "Images")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
fun getCowImageFolder(cowId: String): File {
return File(getImagesBaseFolder(), cowId)
}
fun getVideosFolder(): File {
val folder = File(getBaseFolder(), "Videos")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
}

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

@ -0,0 +1,61 @@
package com.example.livingai.ui.overlay
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.View
class SilhouetteOverlay(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint().apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 5f
}
private val silhouettePaint = Paint().apply {
alpha = 128 // 50% opacity
}
private var silhouette: Bitmap? = null
fun setSilhouette(drawableId: Int) {
try {
if (drawableId != 0) {
silhouette = BitmapFactory.decodeResource(resources, drawableId)
} else {
silhouette = null
}
invalidate()
} catch (e: Exception) {
Log.e("SilhouetteOverlay", "Error loading silhouette", e)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
silhouette?.let { bmp ->
val viewW = width.toFloat()
val viewH = height.toFloat()
val bmpW = bmp.width.toFloat()
val bmpH = bmp.height.toFloat()
// Calculate scale to fit (FIT_CENTER)
val scale = kotlin.math.min(viewW / bmpW, viewH / bmpH)
val scaledW = bmpW * scale
val scaledH = bmpH * scale
val left = (viewW - scaledW) / 2f
val top = (viewH - scaledH) / 2f
val destRect = RectF(left, top, left + scaledW, top + scaledH)
val srcRect = Rect(0, 0, bmp.width, bmp.height)
canvas.drawBitmap(bmp, srcRect, destRect, silhouettePaint)
}
}
}

View File

@ -0,0 +1,501 @@
package com.example.livingai.ui.page
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
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
import java.io.FileOutputStream
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
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)
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationOnClickListener {
finish()
}
updateUILabels()
initializeDefaultMasks()
setupDropdowns()
imagesContainer = findViewById(R.id.currentCowImagesContainer)
currentCowName = savedInstanceState?.getString("COW_NAME") ?: intent.getStringExtra("COW_NAME")
if (currentCowName == null) {
generateNewCowName()
}
loadInitialImages()
if (intent.hasExtra("COW_NAME")) {
loadCowDetails(currentCowName!!)
}
for (orientation in orientationList) {
orientationViews[orientation] = findViewById(resources.getIdentifier(orientation, "id", packageName))
}
findViewById<Button>(R.id.btnNewCow).setOnClickListener {
if (checkStoragePermissions()) {
saveProfile()
} else {
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)
if (cowFolder.exists()) {
cowFolder.listFiles()?.forEach { file ->
if (file.isFile) {
initialImagePaths.add(file.absolutePath)
}
}
}
}
private fun deleteSessionImages() {
val name = currentCowName ?: return
val cowFolder = StorageUtils.getCowImageFolder(name)
if (cowFolder.exists()) {
cowFolder.listFiles()?.forEach { file ->
if (file.isFile && !initialImagePaths.contains(file.absolutePath)) {
file.delete()
}
}
val remaining = cowFolder.listFiles()
if (remaining == null || remaining.isEmpty()) {
cowFolder.delete()
}
}
}
private fun checkStoragePermissions(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
android.os.Environment.isExternalStorageManager()
} else {
val write = ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
val read = ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
write == PackageManager.PERMISSION_GRANTED && read == PackageManager.PERMISSION_GRANTED
}
}
private fun requestStoragePermissions() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
try {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = android.net.Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
} catch (_: Exception) {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, android.Manifest.permission.READ_EXTERNAL_STORAGE),
storagePermissionCode
)
}
}
private fun saveProfile() {
val speciesDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).text.toString()
val breedDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerBreed).text.toString()
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 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 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 {
lines.add(csvRow.trim())
}
FileWriter(csvFile).use { writer ->
lines.forEach { line ->
writer.write(line + "\n")
}
}
Toast.makeText(this, getString(R.string.toast_profile_saved), Toast.LENGTH_SHORT).show()
finish()
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, getString(R.string.toast_error_saving_profile) + " ${e.message}", Toast.LENGTH_SHORT).show()
}
}
private fun loadCowDetails(cowId: String) {
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
if (!csvFile.exists()) return
try {
val lines = csvFile.readLines()
val record = lines.find { it.startsWith("$cowId,") }?.split(",") ?: return
if (record.size >= 8) {
val displaySpecies = record[1]
findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).setText(displaySpecies, false)
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]
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(
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(
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)
}
override fun onResume() {
super.onResume()
refreshCowImages()
}
private fun initializeDefaultMasks() {
val orientationResources = orientationList.associate { orientation ->
val resId = resources.getIdentifier(orientation, "drawable", packageName)
orientation to resId
}
orientationResources.forEach { (orientation, resId) ->
val filename = "${orientation}_mask.png"
val file = File(filesDir, filename)
if (!file.exists()) {
try {
val original = BitmapFactory.decodeResource(resources, resId)
if (original != null) {
val inverted = createInverseBitmask(original)
FileOutputStream(file).use { out ->
inverted.compress(Bitmap.CompressFormat.PNG, 100, out)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun createInverseBitmask(src: Bitmap): Bitmap {
val width = src.width
val height = src.height
val pixels = IntArray(width * height)
src.getPixels(pixels, 0, width, 0, 0, width, height)
for (i in pixels.indices) {
val alpha = (pixels[i] shr 24) and 0xFF
if (alpha > 0) {
pixels[i] = Color.TRANSPARENT
} else {
pixels[i] = Color.BLACK
}
}
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
}
private fun generateNewCowName() {
val sdf = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault())
currentCowName = "cow_${sdf.format(Date())}"
}
private fun refreshCowImages() {
val name = currentCowName ?: return
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)
orientationResourcesPair.forEach { (orientation, pair) ->
val (drawableId, viewId) = pair
val files = if (cowImagesFolder.exists()) {
cowImagesFolder.listFiles { _, fname -> fname.startsWith("${name}_${orientation}_") && fname.endsWith(".jpg") }
} else {
null
}
val latestFile = files?.maxByOrNull { it.lastModified() }
val container = findViewById<LinearLayout>(viewId)
container.removeAllViews()
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
}
if (latestFile != null && latestFile.exists()) {
val frameLayout = android.widget.FrameLayout(this)
frameLayout.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
val imageView = ImageView(this)
imageView.layoutParams = android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.widget.FrameLayout.LayoutParams.MATCH_PARENT
)
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
val bitmap = BitmapFactory.decodeFile(latestFile.absolutePath)
imageView.setImageBitmap(bitmap)
val deleteBtn = ImageView(this)
val btnSize = (24 * resources.displayMetrics.density).toInt()
val btnParams = android.widget.FrameLayout.LayoutParams(btnSize, btnSize)
btnParams.gravity = android.view.Gravity.TOP or android.view.Gravity.END
val margin = (4 * resources.displayMetrics.density).toInt()
btnParams.setMargins(margin, margin, margin, margin)
deleteBtn.layoutParams = btnParams
deleteBtn.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
deleteBtn.setColorFilter(Color.RED)
deleteBtn.setBackgroundColor(Color.parseColor("#80FFFFFF"))
deleteBtn.isClickable = true
deleteBtn.setOnClickListener {
if (latestFile.delete()) {
val remainingFiles = cowImagesFolder.listFiles()
if (remainingFiles == null || remainingFiles.isEmpty()) {
cowImagesFolder.delete()
}
refreshCowImages()
}
}
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 12f
labelView.setTextColor(Color.WHITE)
labelView.setShadowLayer(3f, 0f, 0f, Color.BLACK)
val labelParams = android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
)
labelParams.gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
labelParams.bottomMargin = (4 * resources.displayMetrics.density).toInt()
labelView.layoutParams = labelParams
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)
intent.putExtra("ALLOW_RETAKE", true)
intent.putExtra("COW_NAME", currentCowName)
intent.putExtra("ORIENTATION", orientation)
intent.putExtra("SILHOUETTE_ID", drawableId)
startActivity(intent)
}
container.setOnClickListener(null)
container.isClickable = false
} else {
val iconView = ImageView(this)
val params = LinearLayout.LayoutParams(
(24 * resources.displayMetrics.density).toInt(),
(24 * resources.displayMetrics.density).toInt()
)
params.gravity = android.view.Gravity.CENTER_HORIZONTAL
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,
LinearLayout.LayoutParams.WRAP_CONTENT
)
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"))
container.addView(iconView)
container.addView(textView)
container.setOnClickListener {
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("SILHOUETTE_ID", drawableId)
intent.putExtra("COW_NAME", currentCowName)
intent.putExtra("ORIENTATION", orientation)
startActivity(intent)
}
container.isClickable = true
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("COW_NAME", currentCowName)
}
}

View File

@ -0,0 +1,62 @@
package com.example.livingai.ui.page
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.View
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_full_screen_image)
val imagePath = intent.getStringExtra("IMAGE_PATH")
val allowRetake = intent.getBooleanExtra("ALLOW_RETAKE", false)
val cowName = intent.getStringExtra("COW_NAME")
val orientation = intent.getStringExtra("ORIENTATION")
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
if (imagePath != null) {
val file = File(imagePath)
if (file.exists()) {
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
findViewById<ImageView>(R.id.fullScreenImageView).setImageBitmap(bitmap)
}
}
val btnRetake = findViewById<Button>(R.id.btnRetake)
if (allowRetake && imagePath != null) {
btnRetake.visibility = View.VISIBLE
btnRetake.text = getString(R.string.btn_retake)
btnRetake.setOnClickListener {
// 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)
intent.putExtra("RETAKE_IMAGE_PATH", imagePath) // Pass image path to replace
startActivity(intent)
finish()
}
} else {
btnRetake.visibility = View.GONE
}
val btnBack = findViewById<ImageButton>(R.id.btnBack)
btnBack.contentDescription = getString(R.string.content_desc_back)
btnBack.setOnClickListener {
finish()
}
findViewById<ImageView>(R.id.fullScreenImageView).contentDescription = getString(R.string.content_desc_full_screen_image)
}
}

View File

@ -0,0 +1,387 @@
package com.example.livingai.ui.page
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
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
class GalleryActivity : AppCompatActivity() {
private lateinit var container: LinearLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery)
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationOnClickListener {
finish()
}
container = findViewById(R.id.galleryContainer)
findViewById<FloatingActionButton>(R.id.fabAddCow).setOnClickListener {
val intent = Intent(this, AddAnimalProfileActivity::class.java)
startActivity(intent)
}
refreshGallery()
}
private fun refreshGallery() {
container.removeAllViews()
val imagesBaseFolder = StorageUtils.getImagesBaseFolder()
val cowFolders = imagesBaseFolder.listFiles { file -> file.isDirectory } ?: emptyArray()
val cowNamesFromFolders = cowFolders.map { it.name }
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
val cowDetails = if (csvFile.exists()) {
csvFile.readLines().associate { line ->
val parts = line.split(",")
if (parts.isNotEmpty()) parts[0] to parts else "" to emptyList()
}
} else {
emptyMap()
}
val allCowNames = (cowDetails.keys + cowNamesFromFolders).filter { it.isNotEmpty() && it != "CowID" }.distinct()
allCowNames.forEach { cowName ->
val details = cowDetails[cowName] ?: emptyList()
val cowImageFolder = StorageUtils.getCowImageFolder(cowName)
val cowFiles = cowImageFolder.listFiles { _, name -> name.endsWith(".jpg") }?.toList() ?: emptyList()
addCowSection(cowName, cowFiles, details)
}
}
private fun addCowSection(cowName: String, cowFiles: List<File>, details: List<String>) {
// Main Card
val card = CardView(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, 24)
}
radius = 16 * resources.displayMetrics.density
cardElevation = 2 * resources.displayMetrics.density
setCardBackgroundColor(ContextCompat.getColor(context, R.color.surface_background))
}
// Horizontal Container (3:1 split)
val horizontalContainer = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
weightSum = 4f
setPadding(24, 24, 24, 24)
}
// Left Layout (Info) - Weight 3
val leftLayout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
3f
).apply {
marginEnd = 16
}
}
val nameView = TextView(this).apply {
text = if (details.isNotEmpty()) "${getString(R.string.text_cow_id)} $cowName" else cowName
textSize = 20f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(ContextCompat.getColor(context, R.color.text_header_brown))
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
bottomMargin = 8
}
}
leftLayout.addView(nameView)
if (details.size >= 7) {
// Translate values for display
val displaySpecies = details.getOrElse(1) { "-" }
val displayBreed = details.getOrElse(2) { "-" }
val displayStatus = details.getOrElse(6) { "-" }
val age = details.getOrElse(3) { "-" }
val milk = details.getOrElse(4) { "-" }
val calving = details.getOrElse(5) { "-" }
val infoText = StringBuilder()
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(ContextCompat.getColor(context, R.color.text_brown))
setLineSpacing(10f, 1f)
}
leftLayout.addView(detailsView)
} else {
val detailsView = TextView(this).apply {
text = getString(R.string.text_no_details)
textSize = 14f
setTextColor(ContextCompat.getColor(context, R.color.text_brown))
}
leftLayout.addView(detailsView)
}
horizontalContainer.addView(leftLayout)
// Right Layout (Buttons) - Weight 1
val rightLayout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
gravity = Gravity.CENTER_VERTICAL
}
val buttonParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(48 * resources.displayMetrics.density).toInt()
).apply {
bottomMargin = (8 * resources.displayMetrics.density).toInt()
}
val editButton = MaterialButton(this).apply {
text = getString(R.string.btn_edit)
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(ContextCompat.getColor(context, R.color.text_brown))
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.brown_light_background))
strokeWidth = (1 * resources.displayMetrics.density).toInt()
strokeColor = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.text_brown))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0
minimumHeight = 0
setOnClickListener {
val intent = Intent(this@GalleryActivity, AddAnimalProfileActivity::class.java)
intent.putExtra("COW_NAME", cowName)
startActivity(intent)
}
}
val rateButton = MaterialButton(this).apply {
text = getString(R.string.btn_rate)
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(ContextCompat.getColor(context, R.color.white))
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.earthy))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0
minimumHeight = 0
setOnClickListener {
val intent = Intent(this@GalleryActivity, RatingActivity::class.java)
intent.putExtra("COW_NAME", cowName)
startActivity(intent)
}
}
val deleteButton = MaterialButton(this).apply {
text = getString(R.string.btn_delete)
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(ContextCompat.getColor(context, R.color.white))
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.shutter_red))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0
minimumHeight = 0
setOnClickListener {
deleteCow(cowName)
}
}
rightLayout.addView(editButton)
rightLayout.addView(rateButton)
rightLayout.addView(deleteButton)
horizontalContainer.addView(rightLayout)
card.addView(horizontalContainer)
container.addView(card)
// Orientation Images
if (cowFiles.isNotEmpty()) {
// Instead of separating by orientation, put all images in a grid
val gridLayout = android.widget.GridLayout(this).apply {
columnCount = 3
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(16, 0, 16, 16)
}
}
// Sort images or keep them in order. The user asked for 3 in each row.
cowFiles.forEach { file ->
val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false)
val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail)
val labelView = thumbnailView.findViewById<TextView>(R.id.tvOrientationLabel)
val deleteButtonSmall = thumbnailView.findViewById<android.view.View>(R.id.btnDelete)
// Optionally set layout params for thumbnailView to ensure 3 per row
val displayMetrics = resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val itemWidth = (screenWidth - (48 * displayMetrics.density).toInt()) / 3
thumbnailView.layoutParams = android.widget.GridLayout.LayoutParams().apply {
width = itemWidth
height = itemWidth // Square
setMargins(4, 4, 4, 4)
}
// Extract orientation from filename
val parts = file.name.split("_")
var orientation = ""
if (parts.size >= 3) {
// 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 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
}
labelView.text = label
imageView.setImageBitmap(BitmapFactory.decodeFile(file.absolutePath))
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
imageView.setOnClickListener {
val intent = Intent(this@GalleryActivity, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
}
deleteButtonSmall.setOnClickListener {
if (file.delete()) {
val parentDir = file.parentFile
if (parentDir != null && parentDir.exists()) {
val remaining = parentDir.listFiles()
if (remaining == null || remaining.isEmpty()) {
parentDir.delete()
}
}
Toast.makeText(this@GalleryActivity, getString(R.string.toast_image_deleted), Toast.LENGTH_SHORT).show()
refreshGallery()
} else {
Toast.makeText(this@GalleryActivity, getString(R.string.toast_error_deleting_image), Toast.LENGTH_SHORT).show()
}
}
gridLayout.addView(thumbnailView)
}
container.addView(gridLayout)
val separator = android.view.View(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
2
).apply {
setMargins(0, 16, 0, 16)
}
setBackgroundColor(android.graphics.Color.LTGRAY)
}
container.addView(separator)
}
}
private fun deleteCow(cowName: String) {
// Delete Images
val imageFolder = StorageUtils.getCowImageFolder(cowName)
if (imageFolder.exists()) {
imageFolder.deleteRecursively()
}
val docsFolder = StorageUtils.getDocumentsFolder()
// Delete Profile
val profileFile = File(docsFolder, "cow_profiles.csv")
if (profileFile.exists()) {
val lines = profileFile.readLines()
val newLines = lines.filter { !it.startsWith("$cowName,") }
FileWriter(profileFile).use { writer ->
newLines.forEach { writer.write(it + "\n") }
}
}
// Delete Ratings
val ratingsFile = File(docsFolder, "cow_ratings.csv")
if (ratingsFile.exists()) {
val lines = ratingsFile.readLines()
val newLines = lines.filter { !it.startsWith("$cowName,") }
FileWriter(ratingsFile).use { writer ->
newLines.forEach { writer.write(it + "\n") }
}
}
Toast.makeText(this, getString(R.string.toast_profile_deleted), Toast.LENGTH_SHORT).show()
refreshGallery()
}
override fun onResume() {
super.onResume()
refreshGallery()
}
}

View File

@ -0,0 +1,342 @@
package com.example.livingai.ui.page
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
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 = Constants.ALGORITHM_HAMMING
const val ALGORITHM_EUCLIDEAN = Constants.ALGORITHM_EUCLIDEAN
const val ALGORITHM_JACCARD = Constants.ALGORITHM_JACCARD
private const val PERMISSION_REQUEST_CODE = 101
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
}
private val internalAlgorithms = listOf(ALGORITHM_HAMMING, ALGORITHM_EUCLIDEAN, ALGORITHM_JACCARD)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
setupUI()
checkAndRequestPermissions()
}
private fun checkAndRequestPermissions() {
val permissions = mutableListOf<String>()
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.CAMERA)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+: Request All Files Access for managing external storage
if (!android.os.Environment.isExternalStorageManager()) {
try {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = android.net.Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
} catch (_: Exception) {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
}
} else {
// Android 10 and below
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
if (permissions.isNotEmpty()) {
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), PERMISSION_REQUEST_CODE)
}
}
private fun setupUI() {
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
// Menu Button Logic
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
val btnMenu = findViewById<ImageButton>(R.id.btnMenu)
btnMenu?.setOnClickListener {
drawerLayout.openDrawer(GravityCompat.START)
}
// Language Spinner
val languageSpinner = findViewById<Spinner>(R.id.spinnerLanguage)
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
val savedLang = prefs.getString("LANGUAGE", "English")
languageSpinner.setSelection(languages.indexOf(savedLang))
languageSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedLanguage = languages[position]
val currentLang = prefs.getString("LANGUAGE", "English")
if (selectedLanguage != currentLang) {
saveSettings()
setLanguage(selectedLanguage)
android.os.Handler(android.os.Looper.getMainLooper()).post {
recreate()
}
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
// 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, 4)
setCowIllustration(ivCowIllustration, savedIndex)
ivCowIllustration.setOnClickListener { view ->
showIllustrationPopup(view, ivCowIllustration)
}
// Navigation buttons
findViewById<MaterialButton>(R.id.btnViewGallery).setOnClickListener {
startActivity(Intent(this, GalleryActivity::class.java))
}
findViewById<MaterialButton>(R.id.btnSelectCow).setOnClickListener {
if (!IS_RELEASE_BUILD) {
saveSettings()
}
startActivity(Intent(this, AddAnimalProfileActivity::class.java))
}
// Auto Capture Toggle
val switchAutoCapture = findViewById<SwitchMaterial>(R.id.switchAutoCapture)
switchAutoCapture.text = getString(R.string.label_auto_capture)
val autoCaptureEnabled = prefs.getBoolean(PREF_AUTO_CAPTURE, false)
switchAutoCapture.isChecked = autoCaptureEnabled
val states = arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
)
val thumbColors = intArrayOf(
ContextCompat.getColor(this, R.color.earthy), // Dark Brown for On
Color.GRAY // Grey for Off
)
val trackColors = intArrayOf(
ContextCompat.getColor(this, R.color.switch_track_brown), // Lighter Brown for On track
Color.LTGRAY // Light Grey for Off track
)
switchAutoCapture.thumbTintList = ColorStateList(states, thumbColors)
switchAutoCapture.trackTintList = ColorStateList(states, trackColors)
switchAutoCapture.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(PREF_AUTO_CAPTURE, isChecked).apply()
}
val switchEnableDebug = findViewById<SwitchMaterial>(R.id.switchEnableDebug)
switchEnableDebug.text = getString(R.string.label_enable_debug)
val debugEnabled = prefs.getBoolean(PREF_DEBUG_ENABLE, false)
switchEnableDebug.isChecked = debugEnabled
IS_RELEASE_BUILD = !debugEnabled
switchEnableDebug.thumbTintList = ColorStateList(states, thumbColors)
switchEnableDebug.trackTintList = ColorStateList(states, trackColors)
switchEnableDebug.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(PREF_DEBUG_ENABLE, isChecked).apply()
IS_RELEASE_BUILD = !isChecked
if (IS_RELEASE_BUILD) {
findViewById<CardView>(R.id.viewSettings).visibility = View.GONE
} else {
saveSettings()
findViewById<CardView>(R.id.viewSettings).visibility = View.VISIBLE
}
}
val headerSettings = findViewById<TextView>(R.id.tvHeaderSettings)
if (headerSettings != null) headerSettings.text = getString(R.string.header_settings)
// Algorithm Spinner
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
val displayAlgorithms = listOf(
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)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
// Set default selection from preferences or intent
val savedAlg = prefs.getString("ALGORITHM", ALGORITHM_HAMMING)
val index = internalAlgorithms.indexOf(savedAlg)
spinner.setSelection(if (index >= 0) index else 0)
// Threshold SeekBar
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
val tvThreshold = findViewById<TextView>(R.id.tvThresholdValue)
val savedThreshold = prefs.getInt("THRESHOLD", 75)
seekBar.progress = savedThreshold
tvThreshold.text = "$savedThreshold%"
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
tvThreshold.text = "$progress%"
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
// Mask Display Toggle
val switchMaskDisplay = findViewById<SwitchMaterial>(R.id.switchMaskDisplay)
switchMaskDisplay.text = getString(R.string.label_mask_display)
val maskDisplayEnabled = prefs.getBoolean(PREF_MASK_DISPLAY, false)
switchMaskDisplay.isChecked = maskDisplayEnabled
switchMaskDisplay.thumbTintList = ColorStateList(states, thumbColors)
switchMaskDisplay.trackTintList = ColorStateList(states, trackColors)
switchMaskDisplay.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(PREF_MASK_DISPLAY, isChecked).apply()
}
// Initial visibility based on IS_RELEASE_BUILD
if (IS_RELEASE_BUILD) {
val settingsView = findViewById<CardView>(R.id.viewSettings)
settingsView.visibility = View.GONE
} 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) {
popup.menu.add(0, i, i, "Illustration $i")
}
popup.setOnMenuItemClickListener { item ->
val index = item.itemId
setCowIllustration(imageView, index)
// Save preference
getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE).edit()
.putInt(PREF_COW_ILLUSTRATION_INDEX, index)
.apply()
true
}
popup.show()
}
private fun setCowIllustration(imageView: ImageView, index: Int) {
val resName = "cow_illustration_$index"
val resId = resources.getIdentifier(resName, "drawable", packageName)
if (resId != 0) {
imageView.setImageResource(resId)
} else {
if (index == 0) {
val defaultId = resources.getIdentifier("cow_illustration", "drawable", packageName)
if (defaultId != 0) imageView.setImageResource(defaultId)
}
}
}
private fun saveSettings() {
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
val showSegmentationMask = findViewById<SwitchMaterial>(R.id.switchMaskDisplay)
if (spinner != null && seekBar != null && showSegmentationMask != null) {
val selectedIndex = spinner.selectedItemPosition
val selectedAlgorithm = if (selectedIndex >= 0 && selectedIndex < internalAlgorithms.size) {
internalAlgorithms[selectedIndex]
} else {
ALGORITHM_HAMMING
}
val threshold = seekBar.progress
// Save to preferences
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
val showMask = showSegmentationMask.isChecked
prefs.edit().apply {
putString("ALGORITHM", selectedAlgorithm)
putInt("THRESHOLD", threshold)
putBoolean(PREF_MASK_DISPLAY, showMask)
apply()
}
}
}
private fun saveSettingsAndStart() {
if(!IS_RELEASE_BUILD)
saveSettings()
startActivity(Intent(this, AddAnimalProfileActivity::class.java))
}
}

View File

@ -0,0 +1,310 @@
package com.example.livingai.ui.page
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
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
import java.io.FileWriter
import java.util.Locale
class RatingActivity : AppCompatActivity() {
private lateinit var currentCowName: String
private lateinit var ratingsContainer: LinearLayout
// Feature list (internal keys)
private val features = listOf(
"Stature", "Chest width", "Body depth", "Angularity",
"Rump angle", "Rump width", "Rear legs set", "Rear legs rear view",
"Foot angle", "Fore udder attachment", "Rear udder height",
"Central ligament", "Udder depth", "Front teat position",
"Teat length", "Rear teat position", "Locomotion",
"Body condition score", "Hock development", "Bone structure",
"Rear udder width", "Teat thickness", "Muscularity"
)
private val ratingsMap = mutableMapOf<String, Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rating)
currentCowName = intent.getStringExtra("COW_NAME") ?: run {
finish()
return
}
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationOnClickListener { finish() }
findViewById<TextView>(R.id.tvToolbarTitle)?.text = getString(R.string.title_rate_cow)
setupUIStrings()
loadCowDetails()
loadCowImages()
setupRatingSection()
loadExistingRatings()
findViewById<MaterialButton>(R.id.btnSaveRating).setOnClickListener {
saveRatings()
}
findViewById<MaterialButton>(R.id.btnCancelRating).setOnClickListener {
finish()
}
}
private fun setupUIStrings() {
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() {
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
if (!csvFile.exists()) return
try {
val lines = csvFile.readLines()
val record = lines.find { it.startsWith("$currentCowName,") }?.split(",") ?: return
if (record.size >= 8) {
val displaySpecies = record[1]
val displayBreed = record[2]
val displayStatus = record[6]
val infoText = StringBuilder()
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()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun loadCowImages() {
val container = findViewById<LinearLayout>(R.id.ratingImagesContainer) ?: return
container.removeAllViews()
val cowImagesFolder = StorageUtils.getCowImageFolder(currentCowName)
val files = cowImagesFolder.listFiles { _, name -> name.startsWith("${currentCowName}_") && name.endsWith(".jpg") } ?: return
val horizontalScroll = android.widget.HorizontalScrollView(this)
horizontalScroll.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
horizontalScroll.isFillViewport = false
val imagesLayout = LinearLayout(this)
imagesLayout.orientation = LinearLayout.HORIZONTAL
files.forEach { file ->
val imageView = ImageView(this)
val size = (100 * resources.displayMetrics.density).toInt()
val params = LinearLayout.LayoutParams(size, size)
params.setMargins(0, 0, 16, 0)
imageView.layoutParams = params
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
imageView.setImageBitmap(bitmap)
imageView.setOnClickListener {
val intent = Intent(this, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
}
imagesLayout.addView(imageView)
}
horizontalScroll.addView(imagesLayout)
container.addView(horizontalScroll)
}
private fun setupRatingSection() {
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)
val key = "feature_" + feature.lowercase(Locale.ROOT).replace(" ", "_")
val resourceId = resources.getIdentifier(key, "string", packageName)
val displayName = if (resourceId != 0) getString(resourceId) else feature
featureView.findViewById<TextView>(R.id.tvFeatureName).text = displayName
val buttonContainer = featureView.findViewById<LinearLayout>(R.id.buttonContainer)
val segmentViews = mutableListOf<TextView>()
for (i in 1..9) {
val tv = TextView(this)
val params = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f
)
tv.layoutParams = params
tv.text = i.toString()
tv.gravity = Gravity.CENTER
tv.setTextColor(textColor)
tv.textSize = 14f
tv.setBackgroundColor(Color.TRANSPARENT)
tv.setOnClickListener {
val currentRating = ratingsMap[feature] ?: 0
if (currentRating == i) {
ratingsMap[feature] = 0
updateSegmentSelection(segmentViews, 0)
} else {
ratingsMap[feature] = i
updateSegmentSelection(segmentViews, i)
}
}
segmentViews.add(tv)
buttonContainer.addView(tv)
}
featureView.tag = segmentViews
ratingsContainer.addView(featureView)
}
}
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) {
tv.setTextColor(Color.WHITE)
val radius = (8 * resources.displayMetrics.density)
val drawable = GradientDrawable()
drawable.setColor(selectedColor)
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(textColorUnselected)
tv.background = null
}
}
}
private fun loadExistingRatings() {
val docsFolder = StorageUtils.getDocumentsFolder()
val ratingsFile = File(docsFolder, "cow_ratings.csv")
if (!ratingsFile.exists()) return
try {
val lines = ratingsFile.readLines()
val record = lines.find { it.startsWith("$currentCowName,") }?.split(",") ?: return
if (record.size >= 2) {
val comments = record[1].replace(";", ",")
findViewById<TextInputLayout>(R.id.tilComments).editText?.setText(comments)
features.forEachIndexed { index, feature ->
val ratingStr = record.getOrNull(index + 2)
val rating = ratingStr?.toIntOrNull() ?: 0
if (rating > 0) {
ratingsMap[feature] = rating
val featureView = ratingsContainer.getChildAt(index)
if (featureView != null) {
@Suppress("UNCHECKED_CAST")
val segments = featureView.tag as? List<TextView>
segments?.let { updateSegmentSelection(it, rating) }
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun saveRatings() {
val commentsInput = findViewById<TextInputLayout>(R.id.tilComments).editText?.text.toString()
val comments = commentsInput.replace(",", ";")
val docsFolder = StorageUtils.getDocumentsFolder()
val ratingsFile = File(docsFolder, "cow_ratings.csv")
val headerBuilder = StringBuilder("CowID,Comments")
features.forEach { headerBuilder.append(",$it") }
val header = headerBuilder.toString()
val rowBuilder = StringBuilder()
rowBuilder.append("$currentCowName,$comments")
features.forEach { feature ->
val rating = ratingsMap[feature] ?: 0
rowBuilder.append(",$rating")
}
val newRow = rowBuilder.toString()
try {
val lines = if (ratingsFile.exists()) ratingsFile.readLines().toMutableList() else mutableListOf()
if (lines.isEmpty()) {
lines.add(header)
}
val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") }
if (existingIndex != -1) {
lines[existingIndex] = newRow
} else {
lines.add(newRow)
}
FileWriter(ratingsFile).use { writer ->
lines.forEach { line ->
writer.write(line + "\n")
}
}
Toast.makeText(this, getString(R.string.toast_ratings_saved), Toast.LENGTH_SHORT).show()
finish()
} catch (e: Exception) {
Toast.makeText(this, getString(R.string.toast_error_saving_ratings), Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -0,0 +1,11 @@
package com.example.livingai.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,58 @@
package com.example.livingai.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun LivingAITheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,34 @@
package com.example.livingai.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#5D4037" />
<solid android:color="#FFFFFF" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#EFEBE9" />
<corners android:radius="16dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="1dp" />
<solid android:color="#5D4037" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<!-- Circle Background -->
<path
android:fillColor="#EFEBE9"
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"/>
<!-- Arrow Icon (Centered and Scaled) -->
<group
android:scaleX="1.0"
android:scaleY="1.0"
android:translateX="8.0"
android:translateY="8.0">
<path
android:fillColor="#5D4037"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#EFEBE9" />
<corners android:radius="16dp" />
</shape>

View File

@ -0,0 +1,581 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/surface_background">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface_background"
app:navigationIcon="@drawable/ic_round_back_button">
<TextView
android:id="@+id/tvToolbarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cow Selection"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</androidx.appcompat.widget.Toolbar>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvAddCowDetails"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Cow Details"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="24dp"/>
<!-- Form Fields -->
<TextView
android:id="@+id/tvLabelSpecies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Species"
android:textColor="@color/text_secondary"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSpecies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary">
<AutoCompleteTextView
android:id="@+id/spinnerSpecies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvLabelBreed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Breed"
android:textColor="@color/text_secondary"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBreed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary">
<AutoCompleteTextView
android:id="@+id/spinnerBreed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvLabelAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Age (Years)"
android:textColor="@color/text_secondary"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvLabelMilk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Milk Yield (L)"
android:textColor="@color/text_secondary"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilMilk"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tvLabelCalving"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Calving Number"
android:textColor="@color/text_secondary"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilCalving"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvReproductiveStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reproductive Status"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp"/>
<RadioGroup
android:id="@+id/rgReproductiveStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<RadioButton
android:id="@+id/rbPregnant"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pregnant"
android:layout_marginEnd="16dp"
android:textColor="@color/text_primary"/>
<RadioButton
android:id="@+id/rbCalved"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Calved"
android:layout_marginEnd="16dp"
android:textColor="@color/text_primary"/>
<RadioButton
android:id="@+id/rbNone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="None"
android:textColor="@color/text_primary"/>
</RadioGroup>
<TextView
android:id="@+id/tvLabelDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Description"
android:textColor="@color/text_secondary"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:minLines="3"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Upload Photos -->
<TextView
android:id="@+id/tvUploadPhotos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Upload Photos"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="16dp"/>
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="4"
android:alignmentMode="alignMargins"
android:columnOrderPreserved="false"
android:layout_marginBottom="24dp">
<!-- Front View -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnfront"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvFrontView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Front View"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Rear View (Back) -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnback"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvRearView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rear View"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Left Side -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnleft"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvLeftSide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Left Side"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Right Side -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnright"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvRightSide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Right Side"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Left Angle -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnleftangle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvLeftAngle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Left Angle"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Right Angle -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnrightangle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvRightAngle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Right Angle"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Angle View (Top) - Fixed Size, No Span -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnangle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/input_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/icon_tint_primary"/>
<TextView
android:id="@+id/tvAngleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Angle View"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</GridLayout>
<!-- Container for showing captured thumbnails -->
<LinearLayout
android:id="@+id/currentCowImagesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"/>
<!-- Bottom Buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNewCow"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:text="Save Profile"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/white"
app:cornerRadius="12dp"
app:backgroundTint="@color/earthy"
android:layout_marginEnd="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancel"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:text="Cancel"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/icon_tint_primary"
app:cornerRadius="12dp"
app:backgroundTint="@color/input_background"
app:strokeColor="@color/icon_tint_primary"
app:strokeWidth="1dp"
android:layout_marginStart="8dp"/>
</LinearLayout>
<!-- Hidden TextView for Cow Name logic compatibility -->
<TextView
android:id="@+id/tvCowName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<ImageView
android:id="@+id/fullScreenImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="fitCenter"
android:contentDescription="Full Screen Image" />
<ImageButton
android:id="@+id/btnBack"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_margin="16dp"
android:src="@drawable/ic_round_back_button"
android:background="@android:color/transparent"
android:scaleType="centerInside"
android:contentDescription="Back"
app:tint="@color/white"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnRetake"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="32dp"
android:text="Retake"
android:visibility="gone"
android:textColor="@color/white"
app:backgroundTint="@color/earthy"
app:cornerRadius="12dp"/>
</RelativeLayout>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/page_background">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface_background"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface_background"
app:navigationIcon="@drawable/ic_round_back_button">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Saved Cow Profiles"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</androidx.appcompat.widget.Toolbar>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:boxBackgroundColor="@color/input_background"
app:boxCornerRadiusTopStart="24dp"
app:boxCornerRadiusTopEnd="24dp"
app:boxCornerRadiusBottomStart="24dp"
app:boxCornerRadiusBottomEnd="24dp"
app:boxStrokeWidth="0dp"
app:startIconDrawable="@android:drawable/ic_menu_search"
app:startIconTint="@color/text_secondary"
android:hint="Search profiles..."
android:textColorHint="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginStart="12dp"
app:icon="@android:drawable/ic_menu_sort_by_size"
app:iconTint="@color/icon_tint_primary"
app:backgroundTint="@color/input_background"
app:cornerRadius="12dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp"/>
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/galleryContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"/>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAddCow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="24dp"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/white"
app:backgroundTint="@color/earthy"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,293 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="start">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/page_background"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_horizontal">
<!-- Menu Button (Top Right) to Open Drawer -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_marginBottom="16dp">
<ImageButton
android:id="@+id/btnMenu"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_sort_by_size"
android:background="?attr/selectableItemBackgroundBorderless"
android:backgroundTint="@color/icon_tint_primary"
android:contentDescription="Settings"
app:tint="@color/icon_tint_primary"/>
</LinearLayout>
<!-- Cow Illustration - Circular Logo -->
<androidx.cardview.widget.CardView
android:layout_width="180dp"
android:layout_height="180dp"
app:cardCornerRadius="90dp"
app:cardElevation="8dp"
app:cardBackgroundColor="@color/surface_background"
android:layout_marginBottom="24dp">
<ImageView
android:id="@+id/ivCowIllustration"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/cow_illustration_0"
android:scaleType="centerCrop"
android:contentDescription="Cow Illustration"/>
</androidx.cardview.widget.CardView>
<!-- Title -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cow Data Collector"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:textAlignment="center"
android:layout_marginBottom="8dp"/>
<!-- Subtitle -->
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Record and manage cow information easily."
android:textSize="16sp"
android:textColor="@color/text_tertiary"
android:textAlignment="center"
android:layout_marginBottom="40dp"/>
<!-- Main Buttons Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="32dp">
<!-- Add New Cow Profile Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSelectCow"
android:layout_width="match_parent"
android:layout_height="72dp"
android:text="Add New Cow Profile"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/white"
app:cornerRadius="16dp"
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"/>
<!-- View Saved Profiles Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnViewGallery"
android:layout_width="match_parent"
android:layout_height="72dp"
android:text="View Saved Profiles"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/icon_tint_primary"
app:cornerRadius="16dp"
app:backgroundTint="@color/input_background"
app:strokeColor="@color/icon_tint_primary"
app:strokeWidth="1dp"
app:icon="@android:drawable/ic_menu_gallery"
app:iconTint="@color/icon_tint_primary"
app:iconGravity="textStart"
app:iconPadding="12dp"
android:elevation="4dp"/>
</LinearLayout>
<!-- Settings Card -->
<androidx.cardview.widget.CardView
android:id="@+id/viewSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/surface_background"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tvHeaderSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Debug Configs"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="16dp"/>
<!-- Algorithm Selection -->
<TextView
android:id="@+id/tvAlgorithmLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Algorithm"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp"/>
<FrameLayout
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:backgroundTint="@color/text_primary"/>
</FrameLayout>
<!-- Threshold Slider -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/tvThresholdLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Match Threshold"
android:textSize="14sp"
android:textColor="@color/text_secondary"/>
<TextView
android:id="@+id/tvThresholdValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="75%"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</LinearLayout>
<SeekBar
android:id="@+id/seekBarThreshold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progressTint="@color/earthy"
android:thumbTint="@color/earthy"
android:layout_marginBottom="16dp"/>
<!-- Mask Display Toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchMaskDisplay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Show Segmentation Mask"
android:textColor="@color/text_primary"
android:textSize="14sp"
app:thumbTint="@color/earthy"
app:trackTint="@color/input_background"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
<!-- Navigation Drawer Content (Direct LinearLayout) -->
<LinearLayout
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/surface_background"
android:orientation="vertical"
android:padding="16dp"
android:clickable="true"
android:focusable="true"
android:elevation="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Menu"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="24dp"/>
<!-- Language Selection -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Language"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp"/>
<Spinner
android:id="@+id/spinnerLanguage"
android:layout_width="match_parent"
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 -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchAutoCapture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Auto Capture"
android:textColor="@color/text_primary"
android:textSize="14sp"
app:thumbTint="@color/earthy"
app:trackTint="@color/input_background"
android:layout_marginBottom="16dp"/>
<!-- Enable Debug Toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchEnableDebug"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enable Debug"
android:textColor="@color/text_primary"
android:textSize="14sp"
app:thumbTint="@color/earthy"
app:trackTint="@color/input_background"
android:layout_marginBottom="16dp"/>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -0,0 +1,86 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/cameraPreview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.livingai.ui.overlay.SilhouetteOverlay
android:id="@+id/silhouetteOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.livingai.ui.overlay.MaskOverlay
android:id="@+id/savedMaskOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="5dp"/>
<com.example.livingai.ui.overlay.MaskOverlay
android:id="@+id/segmentationOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
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"
android:layout_height="48dp"
android:layout_gravity="top|start"
android:layout_margin="16dp"
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@drawable/ic_back_arrow"
app:iconSize="24dp"
app:iconTint="@color/white"
app:backgroundTint="@color/camera_overlay_button"
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"/>
<!-- Shutter Button (Initially hidden if Auto) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnShutter"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="32dp"
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@android:drawable/ic_menu_camera"
app:iconSize="40dp"
app:iconTint="@color/white"
app:backgroundTint="@color/shutter_red"
app:cornerRadius="36dp"
app:strokeColor="@color/white"
app:strokeWidth="4dp"
android:padding="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:gravity="center"
android:visibility="gone"/>
</FrameLayout>

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/surface_background">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface_background"
app:navigationIcon="@drawable/ic_round_back_button">
<TextView
android:id="@+id/tvToolbarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rate Cow"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</androidx.appcompat.widget.Toolbar>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvHeaderPhotos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Photos"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/ratingImagesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"
android:minHeight="100dp"/>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/input_background">
<TextView
android:id="@+id/tvHeaderCowDetails"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cow Details"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvCowDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:lineSpacingExtra="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilComments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="@color/text_tertiary"
android:hint="Comments"
android:textColorHint="@color/text_secondary">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:minLines="3"
android:gravity="top"
android:textColor="@color/text_primary"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvHeaderFeatureRatings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Feature Ratings"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/text_primary"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:id="@+id/ratingsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSaveRating"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Save Rating"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/white"
app:cornerRadius="12dp"
app:backgroundTint="@color/earthy"
android:layout_marginBottom="16dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelRating"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Cancel"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/icon_tint_primary"
app:cornerRadius="12dp"
app:backgroundTint="@color/input_background"
app:strokeColor="@color/icon_tint_primary"
app:strokeWidth="1dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvFeatureName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_secondary"
android:layout_marginBottom="8dp"/>
<!-- Rounded Rectangle Container for Segments -->
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
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"/>
</LinearLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<ImageView
android:id="@+id/ivThumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:background="@color/text_tertiary"/>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/tvOrientationLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/camera_overlay_button"
android:textColor="@color/white"
android:textSize="10sp"
android:padding="2dp"
android:gravity="center"
android:elevation="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDelete"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
app:icon="@android:drawable/ic_delete"
app:iconSize="14dp"
app:iconTint="@color/white"
android:backgroundTint="@color/shutter_red"
app:cornerRadius="12dp"
android:padding="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:gravity="center"
android:elevation="6dp"/>
</FrameLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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

@ -0,0 +1,28 @@
<?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="earthy">#6D4C41</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</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">#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

@ -0,0 +1,127 @@
<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

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
</style>
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,17 @@
package com.example.livingai
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Normal file
View File

@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Normal file
View File

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

35
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,35 @@
[versions]
agp = "8.13.1"
kotlin = "2.0.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
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" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
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" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
#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
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Normal file
View File

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

24
settings.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "LivingAI"
include(":app")