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