chore: Halkyon Learn moves into apps/learn

Its server, client, app and curator, and the tools that write its
assets (maps, Anki fixtures, icons), as they are; paths are fixed in
the commits that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 20:08:06 +02:00
commit 649499be23
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
23 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,90 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "app.halkyon.learn"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
applicationId = "app.halkyon.learn"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
// Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION
// is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions)
// You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true`
// flag during build.
versionCode = flutter.versionCode
versionName = flutter.versionName
}
// The app's own key signs release builds when tool/release.sh hands it over
// through the environment: the keystore's path and the key's alias, and the
// password asked for at each release, never written down. Other release builds
// are signed with the debug key, so `flutter run --release` works anywhere; the
// release script refuses to build without the app's key.
val releaseKeystore = System.getenv("HALKYON_ANDROID_KEYSTORE")
signingConfigs {
if (releaseKeystore != null) {
create("release") {
storeFile = file(releaseKeystore)
keyAlias = System.getenv("HALKYON_ANDROID_KEY_ALIAS")
// A PKCS #12 keystore has one password, for the store and its key.
storePassword = System.getenv("HALKYON_ANDROID_KEYSTORE_PASSWORD")
keyPassword = System.getenv("HALKYON_ANDROID_KEYSTORE_PASSWORD")
}
}
}
buildTypes {
release {
signingConfig =
signingConfigs.findByName("release") ?: signingConfigs.getByName("debug")
}
}
// The native libraries go into the APK compressed. One APK serves every phone, so
// it holds them for three architectures; stored uncompressed (the default, made
// for stores that send each phone its own) they made it 110 MB to download and to
// keep. Compressed it's about half that, and Android unpacks the phone's own
// libraries as it installs the app.
packaging {
jniLibs {
useLegacyPackaging = true
}
}
// Leaves out the list of dependencies Gradle would otherwise add to the APK,
// encrypted for Google Play alone; F-Droid's and others' scanners flag it.
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
dependencies {
// NotificationCompat's ProgressStyle and promoted notifications (Android 16's
// Live Updates) for the Pomodoro timer, from v1.17 on; the build resolves this
// version already. v1.19 would need compileSdk 37.
implementation("androidx.core:core:1.18.0")
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,84 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The app syncs with its server: release builds need this too (Flutter's
template grants it to debug and profile builds only). -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- The Pomodoro timer (PomodoroService): its notification, a foreground
service while it runs (no type covers timers: specialUse, as Goodtime),
Android 16's Live Update, the exact alarm at a phase's end (USE_EXACT_ALARM
is granted on install; Google Play would restrict it, F-Droid doesn't), and
Do Not Disturb while focusing, once the person allows it. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<application
android:label="Halkyon Learn"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Receives GitHub's redirect after signing in (app.halkyon.learn://github-callback). -->
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true"
android:taskAffinity="">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="app.halkyon.learn" />
</intent-filter>
</activity>
<service
android:name=".PomodoroService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="A Pomodoro timer the person started, counting down its phase" />
</service>
<receiver
android:name=".PomodoroAlarmReceiver"
android:exported="false" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,111 @@
package app.halkyon.learn
import android.Manifest
import android.app.SearchManager
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.provider.Settings
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private var askedToNotify = false
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor.binaryMessenger
PomodoroService.appBack()
// A question searched with the device's own web search (its default
// browser or search app answers ACTION_WEB_SEARCH); false where none does,
// and the app opens a search engine's address instead.
MethodChannel(messenger, "app.halkyon.learn/search").setMethodCallHandler { call, result ->
when (call.method) {
"webSearch" -> {
val search = Intent(Intent.ACTION_WEB_SEARCH)
.putExtra(SearchManager.QUERY, call.arguments as String)
try {
startActivity(search)
result.success(true)
} catch (_: ActivityNotFoundException) {
result.success(false)
}
}
else -> result.notImplemented()
}
}
// The Pomodoro timer's side of the system (PomodoroService).
MethodChannel(messenger, "app.halkyon.learn/pomodoro").setMethodCallHandler { call, result ->
when (call.method) {
"show" -> {
askToNotify()
PomodoroService.show(this, call.arguments as String)
result.success(null)
}
"clear" -> {
PomodoroService.clear(this)
result.success(null)
}
"takeStoppedOutside" -> result.success(PomodoroService.takeStoppedOutside(this))
"mayKeepQuiet" -> {
val allowed = Quiet.allowed(this)
if (!allowed && call.arguments == true) {
startActivity(Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS))
}
result.success(allowed)
}
else -> result.notImplemented()
}
}
EventChannel(messenger, "app.halkyon.learn/pomodoro/buttons").setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
PomodoroService.buttons = { button -> runOnUiThread { events.success(button) } }
}
override fun onCancel(arguments: Any?) {
PomodoroService.buttons = null
}
},
)
}
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
PomodoroService.buttons = null
PomodoroService.appGone()
super.cleanUpFlutterEngine(flutterEngine)
}
/** Asks once per launch for leave to notify (Android 13 on), which the timer needs. */
private fun askToNotify() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!askedToNotify &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
askedToNotify = true
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), NOTIFY_REQUEST)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// The timer's notification was posted while Android asked, and showed
// nowhere: now it may.
if (requestCode == NOTIFY_REQUEST &&
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
) {
PomodoroService.showAgain()
}
}
private companion object {
const val NOTIFY_REQUEST = 1
}
}

View file

@ -0,0 +1,379 @@
package app.halkyon.learn
import android.Manifest
import android.app.AlarmManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import org.json.JSONObject
// The Pomodoro timer's side of the system (decided 2026-10-07): an ongoing
// notification counting the phase down, with Pause or Resume, Skip and Stop;
// on Android 16 a Live Update whose progress bar is the profile's cycle, the
// phases as segments; an exact alarm that alerts with sound and vibration when
// the phase ends, also with the app closed; and Do Not Disturb while focusing.
//
// The app (Dart) keeps the timer and tells this service what to show (a
// notice, as JSON, already in the app's language). A foreground service keeps
// the process, and with it the app's timer, alive while a phase runs; the
// buttons go to the service, which hands them to the app.
/** Shows the timer as the app's notices say, in the foreground. */
class PomodoroService : Service() {
companion object {
private const val ACTION_SHOW = "app.halkyon.learn.pomodoro.SHOW"
private const val ACTION_BUTTON = "app.halkyon.learn.pomodoro.BUTTON"
private const val EXTRA_NOTICE = "notice"
private const val EXTRA_BUTTON = "button"
const val TIMER_CHANNEL = "pomodoro_timer"
const val ALERT_CHANNEL = "pomodoro_alerts"
private const val TIMER_ID = 4701
const val ALERT_ID = 4702
/** Hands a button pressed (pause, resume, skip, stop) to the app; null while no app listens. */
@Volatile var buttons: ((String) -> Unit)? = null
/** The service while it runs. */
@Volatile private var running: PomodoroService? = null
/** Whether the app is gone (swiped away): its buttons then open it. */
@Volatile private var appGone = false
private const val PREFERENCES = "pomodoro"
private const val STOPPED_OUTSIDE = "stoppedOutside"
/** The app went away: its buttons open it from now on. */
fun appGone() {
appGone = true
running?.repost()
}
/** The app is back, and shows its timer anew. */
fun appBack() {
appGone = false
}
/** Shows the timer again, as once notifications are allowed: posted before, it showed nowhere. */
fun showAgain() {
running?.repost()
}
/** Whether the timer was stopped here while the app was gone; forgets it. */
fun takeStoppedOutside(context: Context): Boolean {
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
val stopped = preferences.getBoolean(STOPPED_OUTSIDE, false)
preferences.edit().remove(STOPPED_OUTSIDE).apply()
return stopped
}
/** Shows [notice] (JSON), starting the service if it doesn't run yet. */
fun show(context: Context, notice: String) {
val service = running
if (service != null) {
service.present(notice)
} else {
ContextCompat.startForegroundService(
context,
Intent(context, PomodoroService::class.java)
.setAction(ACTION_SHOW)
.putExtra(EXTRA_NOTICE, notice),
)
}
}
/** Takes the timer away: the notification, the alarm, the quiet. */
fun clear(context: Context) {
PomodoroAlarm.cancel(context)
Quiet.restore(context)
running?.let {
ServiceCompat.stopForeground(it, ServiceCompat.STOP_FOREGROUND_REMOVE)
it.stopSelf()
}
NotificationManagerCompat.from(context).cancel(TIMER_ID)
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
running = this
}
override fun onDestroy() {
running = null
super.onDestroy()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_SHOW -> intent.getStringExtra(EXTRA_NOTICE)?.let(::present)
ACTION_BUTTON -> {
val button = intent.getStringExtra(EXTRA_BUTTON) ?: return START_NOT_STICKY
val app = buttons
when {
app != null -> app(button)
// The app is gone (swiped away): Stop needs it not, and the app
// forgets its timer when it opens next; the others open it
// (their buttons do so by themselves once the app is gone).
button == "stop" -> {
getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putBoolean(STOPPED_OUTSIDE, true)
.apply()
clear(this)
}
else -> packageManager.getLaunchIntentForPackage(packageName)?.let {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(it)
}
}
}
}
return START_NOT_STICKY
}
/** The last notice shown, to show again as the app comes and goes. */
private var last: JSONObject? = null
/** Shows the last notice again. */
fun repost() {
last?.let { notice ->
NotificationManagerCompat.from(this).let {
if (it.areNotificationsEnabled()) {
try {
it.notify(TIMER_ID, notification(notice))
} catch (_: SecurityException) {
// Notifications not allowed: nothing to show.
}
}
}
}
}
/** Shows [json], schedules its phase's end and keeps the device quiet as it says. */
fun present(json: String) {
val notice = JSONObject(json)
last = notice
channels(this, notice)
ServiceCompat.startForeground(
this,
TIMER_ID,
notification(notice),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
} else {
0
},
)
PomodoroAlarm.schedule(this, notice)
if (notice.optBoolean("quiet")) Quiet.on(this) else Quiet.restore(this)
}
private fun notification(notice: JSONObject): android.app.Notification {
val endsAt = if (notice.isNull("endsAt")) null else notice.getLong("endsAt")
val actions = notice.getJSONObject("actions")
val builder =
NotificationCompat.Builder(this, TIMER_CHANNEL)
.setSmallIcon(R.drawable.ic_stat_pomodoro)
.setContentTitle(notice.getString("title"))
.setContentText(notice.getString("text"))
.setOngoing(true)
.setSilent(true)
.setOnlyAlertOnce(true)
.setCategory(NotificationCompat.CATEGORY_STOPWATCH)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setRequestPromotedOngoing(true)
.setContentIntent(openApp(this))
.setStyle(progress(notice))
if (endsAt != null) {
builder.setUsesChronometer(true).setChronometerCountDown(true).setWhen(endsAt).setShowWhen(true)
builder.addAction(0, actions.getString("pause"), button("pause"))
} else {
builder.setShowWhen(false).setShortCriticalText(countdown(notice.getLong("leftMs")))
builder.addAction(0, actions.getString("resume"), button("resume"))
}
builder.addAction(0, actions.getString("skip"), button("skip"))
builder.addAction(0, actions.getString("stop"), button("stop"))
return builder.build()
}
/** The cycle as a progress bar: each phase a segment, focus in cobalt, breaks in copper. */
private fun progress(notice: JSONObject): NotificationCompat.ProgressStyle {
val cycle = notice.getJSONArray("cycle")
val segments =
(0 until cycle.length()).map { i ->
val phase = cycle.getJSONObject(i)
NotificationCompat.ProgressStyle.Segment((phase.getLong("lengthMs") / 1000).toInt())
.setColor(if (phase.getString("phase") == "focus") 0xFF273EB2.toInt() else 0xFFC44800.toInt())
}
return NotificationCompat.ProgressStyle()
.setProgressSegments(segments)
.setProgress((notice.getLong("elapsedMs") / 1000).toInt())
.setStyledByProgress(true)
}
private fun button(name: String): PendingIntent =
// Without the app, Pause, Resume and Skip open it: a service may not
// start an activity from a notification's button (Android 12 on).
if (appGone && name != "stop") {
openApp(this)!!
} else PendingIntent.getService(
this,
name.hashCode(),
Intent(this, PomodoroService::class.java).setAction(ACTION_BUTTON).putExtra(EXTRA_BUTTON, name),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
/** What is left as a countdown, as the status bar chip shows it while paused. */
private fun countdown(leftMs: Long): String {
val seconds = (leftMs + 999) / 1000
return "%d:%02d".format(seconds / 60, seconds % 60)
}
/** Opens the app where it was. */
internal fun openApp(context: Context): PendingIntent? =
context.packageManager.getLaunchIntentForPackage(context.packageName)?.let {
PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
}
/** The timer's channels, named in the app's language: the countdown, silent; the alerts, with sound and vibration. */
internal fun channels(context: Context, notice: JSONObject) {
context
.getSystemService(NotificationManager::class.java)
.createNotificationChannel(
NotificationChannel(
PomodoroService.TIMER_CHANNEL,
notice.optString("timerName", "Pomodoro"),
NotificationManager.IMPORTANCE_LOW,
).apply { setShowBadge(false) },
)
alertChannel(context, notice.optString("alertsName", "Pomodoro"))
}
/** The alerts' channel, with sound and vibration. */
internal fun alertChannel(context: Context, name: String) {
context
.getSystemService(NotificationManager::class.java)
.createNotificationChannel(
NotificationChannel(PomodoroService.ALERT_CHANNEL, name, NotificationManager.IMPORTANCE_HIGH)
.apply { enableVibration(true) },
)
}
/** The alarm at the end of a phase, which alerts even with the app closed. */
internal object PomodoroAlarm {
private fun intent(context: Context, notice: JSONObject? = null): PendingIntent {
val intent = Intent(context, PomodoroAlarmReceiver::class.java)
if (notice != null) {
intent
.putExtra("title", notice.getString("alertTitle"))
.putExtra("text", notice.getString("alertText"))
.putExtra("alertsName", notice.optString("alertsName", "Pomodoro"))
}
return PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
}
/** Alerts at the end of [notice]'s phase, if it runs; else alerts never. */
fun schedule(context: Context, notice: JSONObject) {
if (notice.isNull("endsAt")) {
cancel(context)
return
}
val alarms = context.getSystemService(AlarmManager::class.java)
val at = notice.getLong("endsAt")
val pending = intent(context, notice)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarms.canScheduleExactAlarms()) {
alarms.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, at, pending)
} else {
// Without exact alarms, Android picks a moment close to it.
alarms.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, at, pending)
}
}
fun cancel(context: Context) {
context.getSystemService(AlarmManager::class.java).cancel(intent(context))
}
}
/** Alerts that a phase ended: sound and vibration, the device's quiet over first. */
class PomodoroAlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Quiet.restore(context)
// The app's own timers may have slept with the device: it looks now.
PomodoroService.buttons?.invoke("phaseEnded")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
return
}
alertChannel(context, intent.getStringExtra("alertsName") ?: "Pomodoro")
NotificationManagerCompat.from(context)
.notify(
PomodoroService.ALERT_ID,
NotificationCompat.Builder(context, PomodoroService.ALERT_CHANNEL)
.setSmallIcon(R.drawable.ic_stat_pomodoro)
.setContentTitle(intent.getStringExtra("title"))
.setContentText(intent.getStringExtra("text"))
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setAutoCancel(true)
.setContentIntent(openApp(context))
.build(),
)
}
}
/**
* Do Not Disturb while focusing, where the person allowed it: the device's
* filter before is kept, and given back afterwards, also after a crash (the
* next notice or clear restores it).
*/
internal object Quiet {
private const val PREFERENCES = "pomodoro"
private const val FILTER_BEFORE = "filterBefore"
fun allowed(context: Context): Boolean =
context.getSystemService(NotificationManager::class.java).isNotificationPolicyAccessGranted
fun on(context: Context) {
if (!allowed(context)) return
val manager = context.getSystemService(NotificationManager::class.java)
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
if (!preferences.contains(FILTER_BEFORE)) {
preferences.edit().putInt(FILTER_BEFORE, manager.currentInterruptionFilter).apply()
}
manager.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
}
fun restore(context: Context) {
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
if (!preferences.contains(FILTER_BEFORE)) return
if (allowed(context)) {
context
.getSystemService(NotificationManager::class.java)
.setInterruptionFilter(preferences.getInt(FILTER_BEFORE, NotificationManager.INTERRUPTION_FILTER_ALL))
}
preferences.edit().remove(FILTER_BEFORE).apply()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The Pomodoro timer's status bar icon: Lucide's timer, white on nothing,
as Android tints it. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M10,2L14,2M12,14L15,11M12,6A8,8 0,1 1,12 22A8,8 0,1 1,12 6Z"
android:strokeWidth="2"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

Some files were not shown because too many files have changed in this diff Show more