Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
}
-keep,includedescriptorclasses class dev.xitee.sleeptimer.navigation.**$$serializer { *; }

# Shizuku AIDL + provider: the AAR ships consumer-proguard rules, but we reference
# rikka.shizuku.ShizukuProvider by fully-qualified name in AndroidManifest.xml.
# The manifest reference keeps the class; this keeps the IShizukuService AIDL stub
# we use in ShizukuShell.
-keep class moe.shizuku.server.IShizukuService { *; }
-keep class moe.shizuku.server.IShizukuService$Stub { *; }
-keep class moe.shizuku.server.IRemoteProcess { *; }
# Shizuku user service: ShellUserService is instantiated by name inside the
# Shizuku-spawned shell process, and the AIDL stub crosses the binder — neither
# may be renamed or stripped.
-keep class dev.xitee.sleeptimer.core.service.shizuku.ShellUserService { *; }
-keep class dev.xitee.sleeptimer.core.service.shizuku.IShellUserService { *; }
-keep class dev.xitee.sleeptimer.core.service.shizuku.IShellUserService$Stub { *; }
26 changes: 26 additions & 0 deletions app/src/main/kotlin/dev/xitee/sleeptimer/di/AppModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.xitee.sleeptimer.di

import android.content.ComponentName
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.xitee.sleeptimer.core.service.screen.DeviceAdminComponent
import dev.xitee.sleeptimer.receiver.SleepTimerDeviceAdminReceiver

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

/**
* Single source of truth for the device-admin receiver's ComponentName. The
* receiver class lives in this module, so lower modules must receive the
* component via injection instead of hard-coding the class name.
*/
@Provides
@DeviceAdminComponent
fun provideDeviceAdminComponent(@ApplicationContext context: Context): ComponentName =
ComponentName(context, SleepTimerDeviceAdminReceiver::class.java)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package dev.xitee.sleeptimer.core.data.di

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.Binds
import dagger.Module
Expand All @@ -16,7 +18,11 @@ import dev.xitee.sleeptimer.core.data.repository.TimerRepository
import dev.xitee.sleeptimer.core.data.repository.TimerRepositoryImpl
import javax.inject.Singleton

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
// A corrupted settings file must degrade to defaults, not crash every collector.
corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() },
)

@Module
@InstallIn(SingletonComponent::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ enum class ThemeId {
val Default: ThemeId = Midnight

fun fromStorage(value: String?): ThemeId =
values().firstOrNull { it.name == value } ?: Default
entries.firstOrNull { it.name == value } ?: Default
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package dev.xitee.sleeptimer.core.data.model

/** Upper bound for any timer duration — shared by the dial UI, the service, and the persisted preset. */
const val MAX_TIMER_MINUTES = 240

data class TimerState(
val phase: TimerPhase = TimerPhase.IDLE,
val totalDurationMillis: Long = 0L,
Expand All @@ -10,5 +13,4 @@ enum class TimerPhase {
IDLE,
RUNNING,
FADING_OUT,
FINISHED,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.xitee.sleeptimer.core.data.model.AutoRotateMode
import dev.xitee.sleeptimer.core.data.model.MAX_TIMER_MINUTES
import dev.xitee.sleeptimer.core.data.model.ThemeId
import dev.xitee.sleeptimer.core.data.model.UserSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -34,7 +38,13 @@ class SettingsRepositoryImpl @Inject constructor(
val PRESET_MINUTES = intPreferencesKey("preset_minutes")
}

override val settings: Flow<UserSettings> = dataStore.data.map { prefs ->
override val settings: Flow<UserSettings> = dataStore.data
.catch { error ->
// A failed read must not crash collectors — SleepTimerService reads this
// flow with runBlocking in onCreate. Fall back to defaults.
if (error is IOException) emit(emptyPreferences()) else throw error
}
.map { prefs ->
// Single source of truth: defaults come from UserSettings(), so adding a new
// field only requires updating the data class.
val d = UserSettings()
Expand All @@ -50,7 +60,9 @@ class SettingsRepositoryImpl @Inject constructor(
starsEnabled = prefs[STARS_ENABLED] ?: d.starsEnabled,
autoRotateMode = AutoRotateMode.fromStorage(prefs[AUTO_ROTATE_MODE]),
stepMinutes = prefs[STEP_MINUTES] ?: d.stepMinutes,
presetMinutes = prefs[PRESET_MINUTES] ?: d.presetMinutes,
// Clamp on read too: values persisted before the cap changed must not
// leak an out-of-range preset into the dial.
presetMinutes = (prefs[PRESET_MINUTES] ?: d.presetMinutes).coerceIn(1, MAX_TIMER_MINUTES),
)
}

Expand Down Expand Up @@ -99,6 +111,6 @@ class SettingsRepositoryImpl @Inject constructor(
}

override suspend fun updatePresetMinutes(minutes: Int) {
dataStore.edit { it[PRESET_MINUTES] = minutes.coerceIn(1, 300) }
dataStore.edit { it[PRESET_MINUTES] = minutes.coerceIn(1, MAX_TIMER_MINUTES) }
}
}
6 changes: 5 additions & 1 deletion core/service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ android {
minSdk = 26
}

buildFeatures {
// For the Shizuku user-service interface (IShellUserService.aidl).
aidl = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
Expand All @@ -31,7 +36,6 @@ dependencies {
ksp(libs.hilt.android.compiler)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.service)

implementation(libs.shizuku.api)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Interface of the Shizuku user service that executes shell commands with
// shell-uid privileges. See ShellUserService for the implementation.
package dev.xitee.sleeptimer.core.service.shizuku;

interface IShellUserService {

void destroy() = 16777114; // Destroy method defined by the Shizuku server

// Runs the command and returns its exit code; negative values signal
// timeout (-2) or failure to launch (-1).
int exec(in String[] command, long timeoutMillis) = 1;
}
Loading
Loading