346 lines
12 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|
|
} |