346 lines
12 KiB
Kotlin

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()
}
}
}
}