android-buschauffeurswekker/app/src/main/java/nl/eddydevink/buschauffeurswekker/MainActivity.kt

184 lines
No EOL
6.9 KiB
Kotlin

package nl.eddydevink.buschauffeurswekker
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler
import nl.eddydevink.buschauffeurswekker.core.Prefs
import nl.eddydevink.buschauffeurswekker.core.ShiftCheck
import nl.eddydevink.buschauffeurswekker.core.ShiftPlanner
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
/**
* Hoofdscherm in de stijl van de systeem-wekker-app: donker thema, één kaart
* per wekker met grote tijd, dag-label en naam. Acties zitten achter het ⋮-menu.
*/
class MainActivity : AppCompatActivity() {
private lateinit var prefs: Prefs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
prefs = Prefs(this)
requestNotificationPermission()
findViewById<TextView>(R.id.tv_menu).setOnClickListener { showMenu(it) }
AlarmScheduler.scheduleNoonCheck(this)
// Installatiehulp/config zonder UI-interactie:
// --es url "…" --es user "…" --es pass "…" → config opslaan + controleren
// --ez test true → testwekker (debugging)
handleSetupIntent(intent)
if (intent?.getBooleanExtra(EXTRA_TEST, false) == true) {
AlarmScheduler.scheduleTest(this)
toast("Testwekker gaat over 10 seconden af")
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleSetupIntent(intent)
if (intent.getBooleanExtra(EXTRA_TEST, false)) {
AlarmScheduler.scheduleTest(this)
toast("Testwekker gaat over 10 seconden af")
}
}
/** Slaat CalDAV-config van intent-extra's op en draait direct een controle. */
private fun handleSetupIntent(intent: Intent) {
val url = intent.getStringExtra(EXTRA_SETUP_URL)
val user = intent.getStringExtra(EXTRA_SETUP_USER)
val pass = intent.getStringExtra(EXTRA_SETUP_PASS)
if (url == null && user == null && pass == null) return
if (!url.isNullOrBlank()) prefs.calendarUrl = url.trim()
if (!user.isNullOrBlank()) prefs.username = user.trim()
if (pass != null) prefs.password = pass
lifecycleScope.launch {
val r = ShiftCheck.run(this@MainActivity)
renderStatus()
toast(r.message)
}
}
override fun onResume() {
super.onResume()
renderStatus()
}
private fun showMenu(anchor: View) {
val popup = PopupMenu(this, anchor)
popup.menu.add(0, MENU_CHECK, 0, "Nu controleren")
popup.menu.add(0, MENU_SETTINGS, 1, "Instellingen")
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
MENU_CHECK -> runCheck()
MENU_SETTINGS -> startActivity(Intent(this, SettingsActivity::class.java))
}
true
}
popup.show()
}
private fun renderStatus() {
val p = prefs
val (day, alarms) = p.loadPlan()
val container = findViewById<LinearLayout>(R.id.alarm_container)
container.removeAllViews()
val inflater = LayoutInflater.from(this)
if (alarms.isEmpty()) {
// Lege state, zoals Alarmio: vertel wat de gebruiker moet doen.
if (p.config() == null) {
container.addView(idleCard(inflater, "Geen instellingen", "Open het menu (⋮) → Instellingen en vul je agenda-gegevens in"))
} else {
container.addView(idleCard(inflater, "Geen wekkers gepland", "Morgen geen dienst"))
}
} else {
val tomorrow = LocalDate.now(ZoneId.systemDefault()).plusDays(1)
val dayLabel = when {
day == tomorrow -> "Morgen"
day != null -> "Gepland ${day.format(DAY_LABEL)}"
else -> "Gepland"
}
alarms.forEach { spec ->
container.addView(
inflater.inflate(R.layout.item_alarm_card, container, false).apply {
findViewById<TextView>(R.id.tv_day).text = dayLabel
findViewById<TextView>(R.id.tv_name).text = spec.name
findViewById<TextView>(R.id.tv_time).text = TIME.format(spec.time)
},
)
}
}
val info = findViewById<TextView>(R.id.tv_shift_info)
val shift = p.lastShiftStartIso?.let { runCatching { LocalDateTime.parse(it) }.getOrNull() }
info.text = if (shift != null) {
val rounded = ShiftPlanner.roundDownToQuarter(shift.toLocalTime())
"Dienst om ${TIME_SHORT.format(shift.toLocalTime())} · afgerond ${TIME_SHORT.format(rounded)}"
} else {
p.lastStatusTxt.orEmpty()
}
}
/** Grijze kaart voor lege toestanden (geen instellingen / geen dienst). */
private fun idleCard(inflater: LayoutInflater, title: String, sub: String): View {
return inflater.inflate(R.layout.item_idle_card, null, false).apply {
findViewById<TextView>(R.id.tv_title).text = title
findViewById<TextView>(R.id.tv_sub).text = sub
}
}
private fun runCheck() {
toast("Bezig met controleren…")
lifecycleScope.launch {
val r = ShiftCheck.run(this@MainActivity)
renderStatus()
toast(r.message)
}
}
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQ_NOTIFICATIONS)
}
}
private fun toast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
private companion object {
const val REQ_NOTIFICATIONS = 42
const val MENU_CHECK = 1
const val MENU_SETTINGS = 2
const val EXTRA_SETUP_URL = "url"
const val EXTRA_SETUP_USER = "user"
const val EXTRA_SETUP_PASS = "pass"
const val EXTRA_TEST = "test"
val TIME = DateTimeFormatter.ofPattern("HH:mm")
val TIME_SHORT = DateTimeFormatter.ofPattern("HH:mm")
val DAY_LABEL = DateTimeFormatter.ofPattern("EEE d MMM", Locale.getDefault())
}
}