build(telefon): default phone app with InCallService, call log, favourites, keypad, details

uhr: handle the public AlarmClock intents (set/show alarm and timer, dismiss, snooze); 0.2.1
docs: how each app replaces the system app

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-17 16:30:12 +02:00
co-authored by Claude Opus 5.5
parent 0f709aaff8
commit 830e653690
45 changed files with 2346 additions and 40 deletions
+16
View File
@@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<application
android:name=".UhrApp"
@@ -28,6 +29,21 @@
</intent-filter>
</activity>
<activity android:name=".AlarmIntentActivity" android:exported="true" android:excludeFromRecents="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:noHistory="true">
<intent-filter>
<action android:name="android.intent.action.SET_ALARM" />
<action android:name="android.intent.action.SET_TIMER" />
<action android:name="android.intent.action.SHOW_ALARMS" />
<action android:name="android.intent.action.SHOW_TIMERS" />
<action android:name="android.intent.action.DISMISS_ALARM" />
<action android:name="android.intent.action.DISMISS_TIMER" />
<action android:name="android.intent.action.SNOOZE_ALARM" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.VOICE" />
</intent-filter>
</activity>
<activity
android:name=".alarm.RingingActivity"
android:exported="false"
@@ -0,0 +1,69 @@
package app.uhr
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.provider.AlarmClock
import android.widget.Toast
import app.uhr.alarm.Ring
import app.uhr.alarm.RingingService
import app.uhr.data.Alarm
import app.uhr.data.Graph
import app.uhr.ui.components.Fmt
import kotlinx.coroutines.runBlocking
import java.util.Calendar
/**
* The public AlarmClock intents, so other apps and voice assistants can use this app
* instead of the system clock: set/show alarms and timers, dismiss and snooze.
*/
class AlarmIntentActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Graph.init(this)
val i = intent
val skipUi = i.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false)
when (i.action) {
AlarmClock.ACTION_SET_ALARM -> setAlarm(i, skipUi)
AlarmClock.ACTION_SET_TIMER -> setTimer(i, skipUi)
AlarmClock.ACTION_SHOW_ALARMS -> open(0)
AlarmClock.ACTION_SHOW_TIMERS -> open(2)
AlarmClock.ACTION_DISMISS_ALARM, AlarmClock.ACTION_DISMISS_TIMER -> RingingService.ringing.value?.let { RingingService.stop(this) } ?: open(0)
AlarmClock.ACTION_SNOOZE_ALARM -> if (RingingService.ringing.value is Ring.AlarmRing) RingingService.snooze(this) else open(0)
else -> open(0)
}
finish()
}
private fun setAlarm(i: Intent, skipUi: Boolean) {
if (!i.hasExtra(AlarmClock.EXTRA_HOUR)) { open(0, newAlarm = true); return }
val hour = i.getIntExtra(AlarmClock.EXTRA_HOUR, 7).coerceIn(0, 23)
val minute = i.getIntExtra(AlarmClock.EXTRA_MINUTES, 0).coerceIn(0, 59)
// EXTRA_DAYS uses Calendar.SUNDAY = 1 … SATURDAY = 7; we store Monday = bit 0
val days = i.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS).orEmpty().fold(0) { acc, d ->
val mondayBased = if (d == Calendar.SUNDAY) 6 else d - 2
if (mondayBased in 0..6) acc or (1 shl mondayBased) else acc
}
val ringtone = i.getStringExtra(AlarmClock.EXTRA_RINGTONE)?.let { if (it == AlarmClock.VALUE_RINGTONE_SILENT) Alarm.SILENT else it }
val alarm = Alarm(
hour = hour, minute = minute, days = days, label = i.getStringExtra(AlarmClock.EXTRA_MESSAGE).orEmpty(),
vibrate = i.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, true), ringtoneUri = ringtone,
)
val id = runBlocking { Graph.repo.save(alarm) }
val inst = runBlocking { Graph.repo.instance(id) }
Fmt.is24h = android.text.format.DateFormat.is24HourFormat(this)
inst?.let { Toast.makeText(this, getString(R.string.alarm_set_in, Fmt.until(resources, it.fireAt)), Toast.LENGTH_SHORT).show() }
if (!skipUi) open(0)
}
private fun setTimer(i: Intent, skipUi: Boolean) {
val seconds = i.getIntExtra(AlarmClock.EXTRA_LENGTH, 0)
if (seconds <= 0) { open(2); return }
runBlocking { Graph.repo.startTimer(seconds * 1000L, i.getStringExtra(AlarmClock.EXTRA_MESSAGE).orEmpty()) }
if (!skipUi) open(2)
}
private fun open(tab: Int, newAlarm: Boolean = false) = startActivity(
Intent(this, MainActivity::class.java).putExtra(MainActivity.EXTRA_TAB, tab).putExtra(MainActivity.EXTRA_NEW_ALARM, newAlarm).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
}
+10 -3
View File
@@ -80,6 +80,7 @@ import java.time.LocalTime
class MainActivity : ComponentActivity() {
private var tabFromIntent = mutableIntStateOf(-1)
private val newAlarm = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -87,15 +88,17 @@ class MainActivity : ComponentActivity() {
Graph.init(this)
Fmt.is24h = DateFormat.is24HourFormat(this)
tabFromIntent.intValue = intent.getIntExtra(EXTRA_TAB, -1)
newAlarm.value = intent.getBooleanExtra(EXTRA_NEW_ALARM, false)
setContent {
val settings by Graph.repo.prefs.settings.collectAsState(initial = Settings())
ClockTheme(settings.theme) { ClockApp(settings, tabFromIntent.intValue) }
ClockTheme(settings.theme) { ClockApp(settings, tabFromIntent.intValue, newAlarm.value) { newAlarm.value = false } }
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
tabFromIntent.intValue = intent.getIntExtra(EXTRA_TAB, -1)
newAlarm.value = intent.getBooleanExtra(EXTRA_NEW_ALARM, false)
}
override fun onResume() {
@@ -103,7 +106,10 @@ class MainActivity : ComponentActivity() {
Fmt.is24h = DateFormat.is24HourFormat(this)
}
companion object { const val EXTRA_TAB = "tab" }
companion object {
const val EXTRA_TAB = "tab"
const val EXTRA_NEW_ALARM = "new_alarm"
}
}
private data class Tab(val title: Int, val icon: ImageVector)
@@ -118,7 +124,7 @@ private val TABS = listOf(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ClockApp(settings: Settings, requestedTab: Int) {
private fun ClockApp(settings: Settings, requestedTab: Int, requestNewAlarm: Boolean = false, onNewAlarmHandled: () -> Unit = {}) {
val repo = Graph.repo
val scope = rememberCoroutineScope()
val context = LocalContext.current
@@ -127,6 +133,7 @@ private fun ClockApp(settings: Settings, requestedTab: Int) {
var showSettings by rememberSaveable { mutableStateOf(false) }
var menu by remember { mutableStateOf(false) }
var addAlarm by remember { mutableStateOf(false) }
LaunchedEffect(requestNewAlarm) { if (requestNewAlarm) { tab = 0; addAlarm = true; onNewAlarmHandled() } }
var addCity by remember { mutableStateOf(false) }
var keypad by rememberSaveable { mutableStateOf(false) }
var digits by rememberSaveable { mutableStateOf("") }