From 0361e739ebd737341b7195ec1aa33958931308b2 Mon Sep 17 00:00:00 2001 From: 202310715280-FADLAN-RIVALDI <202310715280@mhs.ubharajaya.ac.id> Date: Thu, 11 Dec 2025 09:39:56 +0700 Subject: [PATCH] Add Water Level application complete --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/AndroidProjectSystem.xml | 6 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 10 + .idea/deviceManager.xml | 13 + .idea/gradle.xml | 19 + .idea/migrations.xml | 10 + .idea/misc.xml | 10 + .idea/runConfigurations.xml | 17 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle.kts | 63 ++++ app/proguard-rules.pro | 21 ++ .../waterlevel/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 32 ++ .../java/com/example/waterlevel/BubbleView.kt | 158 ++++++++ .../com/example/waterlevel/MainActivity.kt | 346 ++++++++++++++++++ .../com/example/waterlevel/ui/theme/Color.kt | 11 + .../com/example/waterlevel/ui/theme/Theme.kt | 58 +++ .../com/example/waterlevel/ui/theme/Type.kt | 34 ++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ app/src/main/res/layout/activity_main.xml | 145 ++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../com/example/waterlevel/ExampleUnitTest.kt | 17 + build.gradle.kts | 6 + gradle.properties | 23 ++ gradle/libs.versions.toml | 32 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++++ gradlew.bat | 89 +++++ settings.gradle.kts | 23 ++ 50 files changed, 1651 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/waterlevel/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/waterlevel/BubbleView.kt create mode 100644 app/src/main/java/com/example/waterlevel/MainActivity.kt create mode 100644 app/src/main/java/com/example/waterlevel/ui/theme/Color.kt create mode 100644 app/src/main/java/com/example/waterlevel/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/example/waterlevel/ui/theme/Type.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/example/waterlevel/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f35c98b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.waterlevel" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.waterlevel" + 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 + } +} + +dependencies { + + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.core:core-ktx:1.12.0") + 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) + 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) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/waterlevel/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/waterlevel/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..78b9958 --- /dev/null +++ b/app/src/androidTest/java/com/example/waterlevel/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.waterlevel + +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.waterlevel", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7af1091 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/waterlevel/BubbleView.kt b/app/src/main/java/com/example/waterlevel/BubbleView.kt new file mode 100644 index 0000000..bf66e31 --- /dev/null +++ b/app/src/main/java/com/example/waterlevel/BubbleView.kt @@ -0,0 +1,158 @@ +package com.example.waterlevel + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import kotlin.math.min +import kotlin.math.sqrt + +class BubbleView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val backgroundPaint = Paint().apply { + color = Color.parseColor("#2C3E50") + style = Paint.Style.FILL + isAntiAlias = true + } + + private val circlePaint = Paint().apply { + color = Color.parseColor("#34495E") + style = Paint.Style.STROKE + strokeWidth = 8f + isAntiAlias = true + } + + private val crosshairPaint = Paint().apply { + color = Color.parseColor("#7F8C8D") + strokeWidth = 2f + isAntiAlias = true + } + + private val bubblePaint = Paint().apply { + color = Color.parseColor("#27AE60") + style = Paint.Style.FILL + isAntiAlias = true + setShadowLayer(10f, 0f, 0f, Color.BLACK) + } + + private val bubbleBorderPaint = Paint().apply { + color = Color.WHITE + style = Paint.Style.STROKE + strokeWidth = 4f + isAntiAlias = true + } + + private val gridPaint = Paint().apply { + color = Color.parseColor("#34495E") + strokeWidth = 1f + isAntiAlias = true + } + + private var bubbleX = 0f + private var bubbleY = 0f + private var mode = LevelMode.HORIZONTAL + + enum class LevelMode { + HORIZONTAL, VERTICAL, SURFACE + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val centerX = width / 2f + val centerY = height / 2f + val radius = min(width, height) / 2f - 40 + + // Draw background + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint) + + // Draw grid + val gridSpacing = 50f + var i = 0f + while (i < width) { + canvas.drawLine(i, 0f, i, height.toFloat(), gridPaint) + i += gridSpacing + } + i = 0f + while (i < height) { + canvas.drawLine(0f, i, width.toFloat(), i, gridPaint) + i += gridSpacing + } + + when (mode) { + LevelMode.HORIZONTAL, LevelMode.SURFACE -> { + // Draw outer circle + canvas.drawCircle(centerX, centerY, radius, circlePaint) + + // Draw crosshair + canvas.drawLine(centerX - radius, centerY, centerX + radius, centerY, crosshairPaint) + canvas.drawLine(centerX, centerY - radius, centerX, centerY + radius, crosshairPaint) + + // Draw center circle + canvas.drawCircle(centerX, centerY, 30f, circlePaint) + + // Draw bubble + val bubbleRadius = 40f + canvas.drawCircle(centerX + bubbleX, centerY + bubbleY, bubbleRadius, bubblePaint) + canvas.drawCircle(centerX + bubbleX, centerY + bubbleY, bubbleRadius, bubbleBorderPaint) + } + + LevelMode.VERTICAL -> { + // Draw vertical bar + val barWidth = 100f + val barHeight = height - 80f + val barLeft = centerX - barWidth / 2 + val barTop = 40f + + canvas.drawRect(barLeft, barTop, barLeft + barWidth, barTop + barHeight, circlePaint) + + // Draw center line + canvas.drawLine(barLeft, centerY, barLeft + barWidth, centerY, crosshairPaint) + + // Draw bubble + val bubbleHeight = 60f + val bubbleTop = centerY + bubbleY - bubbleHeight / 2 + canvas.drawRect(barLeft + 10, bubbleTop, barLeft + barWidth - 10, bubbleTop + bubbleHeight, bubblePaint) + canvas.drawRect(barLeft + 10, bubbleTop, barLeft + barWidth - 10, bubbleTop + bubbleHeight, bubbleBorderPaint) + } + } + } + + fun updateBubble(pitch: Float, roll: Float) { + // Scale the movement (1 degree = 10 pixels) + val scale = 10f + val maxDistance = min(width, height) / 2f - 80 + + when (mode) { + LevelMode.HORIZONTAL, LevelMode.SURFACE -> { + bubbleX = (roll * scale).coerceIn(-maxDistance, maxDistance) + bubbleY = (pitch * scale).coerceIn(-maxDistance, maxDistance) + } + LevelMode.VERTICAL -> { + bubbleX = 0f + bubbleY = (pitch * scale).coerceIn(-maxDistance, maxDistance) + } + } + + // Change bubble color based on level + val distance = sqrt(bubbleX * bubbleX + bubbleY * bubbleY) + bubblePaint.color = when { + distance < 20 -> Color.parseColor("#27AE60") // Green - Level + distance < 50 -> Color.parseColor("#F39C12") // Orange - Close + else -> Color.parseColor("#E74C3C") // Red - Not level + } + + invalidate() + } + + fun setMode(newMode: LevelMode) { + mode = newMode + invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/waterlevel/MainActivity.kt b/app/src/main/java/com/example/waterlevel/MainActivity.kt new file mode 100644 index 0000000..7072a34 --- /dev/null +++ b/app/src/main/java/com/example/waterlevel/MainActivity.kt @@ -0,0 +1,346 @@ +package com.example.waterlevel + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.hardware.camera2.CameraManager +import android.media.AudioManager +import android.media.ToneGenerator +import android.os.Build +import android.os.Bundle +import android.os.VibrationEffect +import android.os.Vibrator +import android.util.Log +import android.view.View +import android.view.WindowManager +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.abs +import kotlin.math.sqrt + +class MainActivity : AppCompatActivity(), SensorEventListener { + + private lateinit var sensorManager: SensorManager + private var accelerometer: Sensor? = null + private var magnetometer: Sensor? = null + + private lateinit var bubbleView: BubbleView + private lateinit var angleTextView: TextView + private lateinit var statusTextView: TextView + private lateinit var modeSpinner: Spinner + private lateinit var calibrateButton: Button + private lateinit var saveButton: Button + private lateinit var flashlightButton: Button + private lateinit var soundSwitch: Switch + private lateinit var vibrateSwitch: Switch + private lateinit var historyButton: Button + + private var gravity = FloatArray(3) + private var geomagnetic = FloatArray(3) + private var calibrationOffset = floatArrayOf(0f, 0f) + + private var cameraManager: CameraManager? = null + private var cameraId: String? = null + private var isFlashlightOn = false + + // Sound & Vibrate + private var toneGenerator: ToneGenerator? = null + private var lastFeedbackTime = 0L + private val FEEDBACK_INTERVAL = 1000L // 1 detik + + private val CAMERA_PERMISSION_CODE = 100 + private val LEVEL_THRESHOLD = 2.0f // Derajat untuk dianggap "level" + + private var currentMode = BubbleView.LevelMode.HORIZONTAL + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Keep screen on + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + initViews() + initSensors() + setupListeners() + + // Initialize ToneGenerator for sound + toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100) + } + + private fun initViews() { + bubbleView = findViewById(R.id.bubbleView) + angleTextView = findViewById(R.id.angleTextView) + statusTextView = findViewById(R.id.statusTextView) + modeSpinner = findViewById(R.id.modeSpinner) + calibrateButton = findViewById(R.id.calibrateButton) + saveButton = findViewById(R.id.saveButton) + flashlightButton = findViewById(R.id.flashlightButton) + soundSwitch = findViewById(R.id.soundSwitch) + vibrateSwitch = findViewById(R.id.vibrateSwitch) + historyButton = findViewById(R.id.historyButton) + + // Setup spinner + val modes = arrayOf("Horizontal Level", "Vertical Level", "Surface Angle") + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, modes) + modeSpinner.adapter = adapter + } + + private fun initSensors() { + sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager + accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + + // Initialize camera manager for flashlight + cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager + try { + cameraId = cameraManager?.cameraIdList?.get(0) + } catch (e: Exception) { + e.printStackTrace() + } + + if (accelerometer == null) { + Toast.makeText(this, "Accelerometer not available!", Toast.LENGTH_LONG).show() + } + } + + private fun setupListeners() { + modeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + currentMode = when (position) { + 0 -> BubbleView.LevelMode.HORIZONTAL + 1 -> BubbleView.LevelMode.VERTICAL + 2 -> BubbleView.LevelMode.SURFACE + else -> BubbleView.LevelMode.HORIZONTAL + } + bubbleView.setMode(currentMode) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + + calibrateButton.setOnClickListener { + calibrate() + } + + saveButton.setOnClickListener { + saveMeasurement() + } + + flashlightButton.setOnClickListener { + toggleFlashlight() + } + + historyButton.setOnClickListener { + showHistory() + } + } + + override fun onResume() { + super.onResume() + accelerometer?.also { acc -> + sensorManager.registerListener(this, acc, SensorManager.SENSOR_DELAY_UI) + } + magnetometer?.also { mag -> + sensorManager.registerListener(this, mag, SensorManager.SENSOR_DELAY_UI) + } + } + + override fun onPause() { + super.onPause() + sensorManager.unregisterListener(this) + } + + override fun onSensorChanged(event: SensorEvent?) { + if (event == null) return + + when (event.sensor.type) { + Sensor.TYPE_ACCELEROMETER -> { + gravity = event.values.clone() + Log.d("WaterLevel", "Accel: ${gravity[0]}, ${gravity[1]}, ${gravity[2]}") + } + Sensor.TYPE_MAGNETIC_FIELD -> { + geomagnetic = event.values.clone() + Log.d("WaterLevel", "Mag: ${geomagnetic[0]}, ${geomagnetic[1]}, ${geomagnetic[2]}") + } + } + + val R = FloatArray(9) + val I = FloatArray(9) + + if (SensorManager.getRotationMatrix(R, I, gravity, geomagnetic)) { + val orientation = FloatArray(3) + SensorManager.getOrientation(R, orientation) + + // Convert radians to degrees + val pitch = Math.toDegrees(orientation[1].toDouble()).toFloat() + val roll = Math.toDegrees(orientation[2].toDouble()).toFloat() + + Log.d("WaterLevel", "Pitch: $pitch, Roll: $roll") + + // Apply calibration + val adjustedPitch = pitch - calibrationOffset[0] + val adjustedRoll = roll - calibrationOffset[1] + + updateUI(adjustedPitch, adjustedRoll) + } else { + Log.e("WaterLevel", "getRotationMatrix FAILED!") + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not needed for this app + } + + private fun updateUI(pitch: Float, roll: Float) { + val angle = when (currentMode) { + BubbleView.LevelMode.HORIZONTAL -> sqrt(pitch * pitch + roll * roll) + BubbleView.LevelMode.VERTICAL -> abs(pitch) + BubbleView.LevelMode.SURFACE -> sqrt(pitch * pitch + roll * roll) + } + + bubbleView.updateBubble(pitch, roll) + angleTextView.text = String.format("%.1f°", angle) + + // Check if level + val isLevel = angle < LEVEL_THRESHOLD + statusTextView.text = if (isLevel) "LEVEL ✓" else "NOT LEVEL" + statusTextView.setTextColor( + if (isLevel) ContextCompat.getColor(this, android.R.color.holo_green_dark) + else ContextCompat.getColor(this, android.R.color.holo_red_dark) + ) + + // Feedback - cuma trigger tiap 1 detik biar gak spam + if (isLevel) { + val currentTime = System.currentTimeMillis() + + if (currentTime - lastFeedbackTime > FEEDBACK_INTERVAL) { + // Sound + if (soundSwitch.isChecked) { + try { + toneGenerator?.startTone(ToneGenerator.TONE_PROP_BEEP, 100) + } catch (e: Exception) { + e.printStackTrace() + } + } + + // Vibrate + if (vibrateSwitch.isChecked) { + vibrateOnce() + } + + lastFeedbackTime = currentTime + } + } + } + + private fun calibrate() { + val R = FloatArray(9) + val I = FloatArray(9) + + if (SensorManager.getRotationMatrix(R, I, gravity, geomagnetic)) { + val orientation = FloatArray(3) + SensorManager.getOrientation(R, orientation) + + calibrationOffset[0] = Math.toDegrees(orientation[1].toDouble()).toFloat() + calibrationOffset[1] = Math.toDegrees(orientation[2].toDouble()).toFloat() + + Toast.makeText(this, "Calibrated!", Toast.LENGTH_SHORT).show() + } + } + + private fun saveMeasurement() { + val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date()) + val angle = angleTextView.text.toString() + val mode = modeSpinner.selectedItem.toString() + + // Save to SharedPreferences or Database + val prefs = getSharedPreferences("measurements", Context.MODE_PRIVATE) + val editor = prefs.edit() + val existing = prefs.getStringSet("history", mutableSetOf()) ?: mutableSetOf() + existing.add("$timestamp | $mode | $angle") + editor.putStringSet("history", existing) + editor.apply() + + Toast.makeText(this, "Measurement saved!", Toast.LENGTH_SHORT).show() + } + + private fun showHistory() { + val prefs = getSharedPreferences("measurements", Context.MODE_PRIVATE) + val history = prefs.getStringSet("history", mutableSetOf()) ?: mutableSetOf() + + if (history.isEmpty()) { + Toast.makeText(this, "No saved measurements", Toast.LENGTH_SHORT).show() + return + } + + val historyText = history.joinToString("\n") + + val builder = android.app.AlertDialog.Builder(this) + builder.setTitle("Measurement History") + builder.setMessage(historyText) + builder.setPositiveButton("OK", null) + builder.setNegativeButton("Clear All") { _, _ -> + prefs.edit().remove("history").apply() + Toast.makeText(this, "History cleared", Toast.LENGTH_SHORT).show() + } + builder.show() + } + + private fun toggleFlashlight() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_CODE) + return + } + + try { + if (cameraId != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + cameraManager?.setTorchMode(cameraId!!, !isFlashlightOn) + isFlashlightOn = !isFlashlightOn + flashlightButton.text = if (isFlashlightOn) "Flashlight ON" else "Flashlight OFF" + } + } + } catch (e: Exception) { + Toast.makeText(this, "Flashlight not available", Toast.LENGTH_SHORT).show() + e.printStackTrace() + } + } + + private fun vibrateOnce() { + val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(50) + } + } + + override fun onDestroy() { + super.onDestroy() + + // Release ToneGenerator + toneGenerator?.release() + toneGenerator = null + + // Turn off flashlight + if (isFlashlightOn && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + cameraId?.let { cameraManager?.setTorchMode(it, false) } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/waterlevel/ui/theme/Color.kt b/app/src/main/java/com/example/waterlevel/ui/theme/Color.kt new file mode 100644 index 0000000..5667d3f --- /dev/null +++ b/app/src/main/java/com/example/waterlevel/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.waterlevel.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) \ No newline at end of file diff --git a/app/src/main/java/com/example/waterlevel/ui/theme/Theme.kt b/app/src/main/java/com/example/waterlevel/ui/theme/Theme.kt new file mode 100644 index 0000000..629346e --- /dev/null +++ b/app/src/main/java/com/example/waterlevel/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.example.waterlevel.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 WaterlevelTheme( + 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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/waterlevel/ui/theme/Type.kt b/app/src/main/java/com/example/waterlevel/ui/theme/Type.kt new file mode 100644 index 0000000..e218363 --- /dev/null +++ b/app/src/main/java/com/example/waterlevel/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.waterlevel.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 + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1c90049 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +