Skip to content

Commit a5f1f93

Browse files
authored
Merge pull request #80 from xLexip/develop
v0.11.0-beta
2 parents aada8b2 + 5538891 commit a5f1f93

File tree

14 files changed

+154
-71
lines changed

14 files changed

+154
-71
lines changed

.github/workflows/build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
env:
1919
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
2020
MOCK_DARK_THEME_HANDLER: ${{ secrets.MOCK_DARK_THEME_HANDLER }}
21+
ANALYTICS_GATE: ${{ secrets.ANALYTICS_GATE }}
2122
run: |
2223
# Fail fast if secrets are missing
2324
if [ -z "${GOOGLE_SERVICES_JSON:-}" ]; then
@@ -28,15 +29,21 @@ jobs:
2829
echo "Missing MOCK_DARK_THEME_HANDLER" >&2
2930
exit 1
3031
fi
32+
if [ -z "${ANALYTICS_GATE:-}" ]; then
33+
echo "Missing ANALYTICS_GATE" >&2
34+
exit 1
35+
fi
3136
3237
# Ensure directories
3338
mkdir -p app
3439
mkdir -p app/src/main/java/dev/lexip/hecate/util
40+
mkdir -p app/src/main/java/dev/lexip/hecate/analytics
3541
3642
# Safely write files
3743
printf '%s' "$GOOGLE_SERVICES_JSON" > google-services.json
3844
printf '%s' "$GOOGLE_SERVICES_JSON" > app/google-services.json
3945
printf '%s' "$MOCK_DARK_THEME_HANDLER" > app/src/main/java/dev/lexip/hecate/util/DarkThemeHandler.kt
46+
printf '%s' "$ANALYTICS_GATE" > app/src/main/java/dev/lexip/hecate/analytics/AnalyticsGate.kt
4047
- name: Set up JDK 23
4148
uses: actions/setup-java@v5
4249
with:

.gitignore

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,15 @@ render.experimental.xml
2727
*.jks
2828
*.keystore
2929

30-
# Google Services (e.g. APIs or Firebase)
30+
# Secrets (e.g. APIs or Firebase)
3131
google-services.json
32-
33-
# Trade secret
34-
app/src/main/java/dev/lexip/hecate/util/DarkThemeHandler.kt
32+
**/AnalyticsGate.kt
33+
**/DarkThemeHandler.kt
3534

3635
# Android Profiling
3736
*.hprof
3837

3938
# Builds
4039
app/release/
4140
app/debug/
42-
app/beta/
41+
app/beta/

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ android {
1515
defaultConfig {
1616
applicationId = "dev.lexip.hecate"
1717
minSdk = 34
18-
targetSdk = 36
18+
targetSdk = 35
1919
versionCode = 63
2020
versionName = "0.10.1"
2121
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

app/src/main/java/dev/lexip/hecate/Application.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import android.content.Context
1717
import androidx.datastore.core.DataStore
1818
import androidx.datastore.preferences.core.Preferences
1919
import androidx.datastore.preferences.preferencesDataStore
20-
import com.google.firebase.crashlytics.FirebaseCrashlytics
2120
import dev.lexip.hecate.analytics.AnalyticsGate
2221

2322
const val USER_PREFERENCES_NAME = "user_preferences"
@@ -34,9 +33,5 @@ class Application : Application() {
3433
override fun onCreate() {
3534
super.onCreate()
3635
AnalyticsGate.init(this)
37-
38-
if (BuildConfig.DEBUG) {
39-
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = false
40-
}
4136
}
4237
}

app/src/main/java/dev/lexip/hecate/services/BroadcastReceiverService.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import android.app.PendingIntent
1919
import android.app.Service
2020
import android.content.Intent
2121
import android.content.IntentFilter
22+
import android.content.pm.ServiceInfo
2223
import android.os.IBinder
2324
import android.provider.Settings
2425
import android.util.Log
@@ -39,6 +40,7 @@ import kotlinx.coroutines.launch
3940
private const val TAG = "BroadcastReceiverService"
4041
private const val NOTIFICATION_CHANNEL_ID = "ForegroundServiceChannel"
4142
private const val ACTION_PAUSE_SERVICE = "dev.lexip.hecate.action.STOP_SERVICE"
43+
internal const val EXTRA_ENABLE_MONITORING = "dev.lexip.hecate.extra.ENABLE_MONITORING"
4244

4345
private var screenOnReceiver: ScreenOnReceiver? = null
4446

@@ -82,15 +84,29 @@ class BroadcastReceiverService : Service() {
8284
// Start foreground immediately to comply with O+ requirements
8385
createNotificationChannel()
8486
val initialNotification = buildNotification()
85-
startForeground(1, initialNotification)
87+
try {
88+
startForeground(
89+
1,
90+
initialNotification,
91+
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
92+
)
93+
} catch (e: Exception) {
94+
/**
95+
* Catch required because some Android 14 ROMs (HyperOS/MIUI) are broken
96+
* and throw false-positive SecurityExceptions for valid FGS types.
97+
*/
98+
startForeground(1, initialNotification)
99+
}
100+
86101

87102
// Load user preferences from data store
88103
serviceScope.launch {
89104
val userPreferencesRepository = UserPreferencesRepository(dataStore)
90105
val userPreferences = userPreferencesRepository.fetchInitialPreferences()
91106

92107
// Create screen-on receiver if adaptive theme is enabled
93-
if (userPreferences.adaptiveThemeEnabled) {
108+
val forceEnable = intent?.getBooleanExtra(EXTRA_ENABLE_MONITORING, false) == true
109+
if (userPreferences.adaptiveThemeEnabled || forceEnable) {
94110
createScreenOnReceiver(userPreferences.adaptiveThemeThresholdLux)
95111
}
96112

app/src/main/java/dev/lexip/hecate/services/QuickSettingsTileService.kt

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,28 @@ import android.content.Intent
1717
import android.content.pm.PackageManager
1818
import android.service.quicksettings.Tile
1919
import android.service.quicksettings.TileService
20+
import android.util.Log
2021
import androidx.core.content.ContextCompat
2122
import dev.lexip.hecate.Application
2223
import dev.lexip.hecate.analytics.AnalyticsLogger
2324
import dev.lexip.hecate.data.UserPreferencesRepository
25+
import dev.lexip.hecate.util.DefaultDispatcherProvider
26+
import dev.lexip.hecate.util.DispatcherProvider
2427
import kotlinx.coroutines.CoroutineScope
25-
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.Job
2629
import kotlinx.coroutines.SupervisorJob
2730
import kotlinx.coroutines.launch
2831

29-
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
32+
private val serviceScope = CoroutineScope(SupervisorJob() + DefaultDispatcherProvider.main)
33+
private const val TAG = "QuickSettingsTileService"
3034

3135
class QuickSettingsTileService : TileService() {
3236

37+
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider
38+
39+
private var listeningJob: Job? = null
40+
private var toggleJob: Job? = null
41+
3342
private fun hasWriteSecureSettingsPermission(): Boolean {
3443
return packageManager.checkPermission(
3544
Manifest.permission.WRITE_SECURE_SETTINGS,
@@ -46,54 +55,66 @@ class QuickSettingsTileService : TileService() {
4655
super.onStartListening()
4756
val tile = qsTile ?: return
4857

49-
// No permission => tile unavailable
5058
if (!hasWriteSecureSettingsPermission()) {
5159
tile.state = Tile.STATE_UNAVAILABLE
5260
tile.updateTile()
5361
return
5462
}
5563

56-
// Load user preference and set tile state
57-
val dataStore = (applicationContext as Application).userPreferencesDataStore
58-
val repo = UserPreferencesRepository(dataStore)
59-
60-
serviceScope.launch {
64+
listeningJob?.cancel()
65+
listeningJob = serviceScope.launch {
66+
// Load user preference and set tile state
67+
val dataStore = (applicationContext as Application).userPreferencesDataStore
68+
val repo = UserPreferencesRepository(dataStore)
6169
val prefs = repo.fetchInitialPreferences()
6270
tile.state = if (prefs.adaptiveThemeEnabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
6371
tile.updateTile()
6472
}
6573
}
6674

75+
override fun onStopListening() {
76+
super.onStopListening()
77+
listeningJob?.cancel()
78+
}
79+
6780
override fun onClick() {
6881
super.onClick()
6982
val tile = qsTile ?: return
7083

71-
// No permission => tile unavailable
72-
if (!hasWriteSecureSettingsPermission()) {
73-
tile.state = Tile.STATE_UNAVAILABLE
74-
tile.updateTile()
75-
return
76-
}
77-
78-
val dataStore = (applicationContext as Application).userPreferencesDataStore
79-
val repo = UserPreferencesRepository(dataStore)
84+
listeningJob?.cancel()
85+
toggleJob?.cancel()
8086

8187
// Toggle adaptive theme
82-
serviceScope.launch {
83-
val prefs = repo.fetchInitialPreferences()
84-
val newEnabled = !prefs.adaptiveThemeEnabled
88+
val isEnabled = tile.state == Tile.STATE_ACTIVE
89+
val newEnabled = !isEnabled
8590

86-
repo.updateAdaptiveThemeEnabled(newEnabled)
91+
// Update tile UI immediately
92+
tile.state = if (newEnabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
93+
tile.updateTile()
94+
95+
toggleJob = serviceScope.launch(dispatchers.io) {
96+
val dataStore = (applicationContext as Application).userPreferencesDataStore
97+
val repo = UserPreferencesRepository(dataStore)
8798

8899
// Start/stop the service
89100
val intent = Intent(applicationContext, BroadcastReceiverService::class.java)
90101
if (newEnabled) {
91-
repo.ensureAdaptiveThemeThresholdDefault()
92-
ContextCompat.startForegroundService(applicationContext, intent)
93-
AnalyticsLogger.logServiceEnabled(
94-
applicationContext,
95-
source = "quick_settings_tile"
96-
)
102+
intent.putExtra(EXTRA_ENABLE_MONITORING, true)
103+
try {
104+
ContextCompat.startForegroundService(applicationContext, intent)
105+
AnalyticsLogger.logServiceEnabled(
106+
applicationContext,
107+
source = "quick_settings_tile"
108+
)
109+
} catch (e: Exception) {
110+
Log.e(TAG, "Failed to start service", e)
111+
// Revert UI if service start fails
112+
launch(dispatchers.main) {
113+
tile.state = Tile.STATE_INACTIVE
114+
tile.updateTile()
115+
}
116+
return@launch
117+
}
97118
} else {
98119
applicationContext.stopService(intent)
99120
AnalyticsLogger.logServiceDisabled(
@@ -102,10 +123,17 @@ class QuickSettingsTileService : TileService() {
102123
)
103124
}
104125

105-
// Update tile UI
106-
tile.state = if (newEnabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
107-
tile.updateTile()
126+
if (newEnabled) {
127+
repo.ensureAdaptiveThemeThresholdDefault()
128+
}
129+
repo.updateAdaptiveThemeEnabled(newEnabled)
108130
}
109131
}
110132

133+
override fun onDestroy() {
134+
super.onDestroy()
135+
listeningJob?.cancel()
136+
toggleJob?.cancel()
137+
}
138+
111139
}

app/src/main/java/dev/lexip/hecate/ui/AppNavHost.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import dev.lexip.hecate.ui.setup.SetupViewModelFactory
4646
import dev.lexip.hecate.ui.setup.screens.A_DeveloperModeScreen
4747
import dev.lexip.hecate.ui.setup.screens.B_ConnectUsbScreen
4848
import dev.lexip.hecate.ui.setup.screens.C_GrantPermissionScreen
49+
import dev.lexip.hecate.ui.setup.shareText
4950

5051
@Composable
5152
fun AppNavHost(
@@ -230,7 +231,12 @@ private fun NavGraphBuilder.setupNavGraph(
230231

231232
C_GrantPermissionScreen(
232233
uiState = setupUiState,
233-
onShareSetupUrl = setupViewModel::shareSetupUrl,
234+
onShareSetupUrl = {
235+
context.shareText(
236+
"https://lexip.dev/setup",
237+
"Setup - Adaptive Theme"
238+
)
239+
},
234240
onShareExpertCommand = setupViewModel::shareAdbCommand,
235241
onFinish = setupViewModel::checkPermissionAndComplete,
236242
onBack = setupViewModel::navigateBack,

app/src/main/java/dev/lexip/hecate/ui/MainActivity.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,6 @@ class MainActivity : ComponentActivity() {
6868
)
6969
}
7070
}
71-
72-
inAppUpdateManager?.checkForImmediateUpdate()
73-
inAppUpdateManager?.checkForFlexibleUpdate()
7471
inAppUpdateManager?.checkAndLaunchUpdate()
7572
}
7673

app/src/main/java/dev/lexip/hecate/ui/setup/SetupViewModel.kt

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import dev.lexip.hecate.Application
3131
import dev.lexip.hecate.R
3232
import dev.lexip.hecate.analytics.AnalyticsLogger
3333
import dev.lexip.hecate.data.UserPreferencesRepository
34+
import dev.lexip.hecate.services.BroadcastReceiverService
3435
import dev.lexip.hecate.ui.navigation.NavigationEvent
3536
import dev.lexip.hecate.ui.navigation.NavigationManager
3637
import dev.lexip.hecate.ui.navigation.SetupRoute
@@ -418,20 +419,30 @@ class SetupViewModel(
418419
fun completeSetup(source: String? = null) {
419420
viewModelScope.launch {
420421
if (setupCompletionHandled.getAndSet(true)) return@launch
422+
_uiState.update { it.copy(isSetupCompleted = true) }
423+
stopEnvironmentMonitoring()
421424

422425
val context = application.applicationContext
423426
AnalyticsLogger.logSetupComplete(context, source)
424427

425-
428+
// Activate Adaptive Theme
426429
withContext(ioDispatcher) {
427430
userPreferencesRepository.updateSetupCompleted(true)
428431
userPreferencesRepository.ensureAdaptiveThemeThresholdDefault()
429-
delay(300L) // Delay for animations
430432
userPreferencesRepository.updateAdaptiveThemeEnabled(true)
433+
434+
// Start Service
435+
val intent =
436+
Intent(application.applicationContext, BroadcastReceiverService::class.java)
437+
ContextCompat.startForegroundService(application.applicationContext, intent)
438+
439+
AnalyticsLogger.logServiceEnabled(
440+
application.applicationContext,
441+
source = "setup_complete"
442+
)
431443
}
432444

433-
_uiState.update { it.copy(isSetupCompleted = true) }
434-
stopEnvironmentMonitoring()
445+
// Close Setup
435446
navigationManager.tryNavigate(NavigationEvent.ToMainClearingSetup)
436447
}
437448
}
@@ -463,17 +474,7 @@ class SetupViewModel(
463474
})
464475
}
465476
}
466-
467-
fun shareSetupUrl() {
468-
val context = application.applicationContext
469-
AnalyticsLogger.logShareLinkClicked(context, "setup")
470-
val intent = Intent(Intent.ACTION_VIEW).apply {
471-
data = "https://hecate.lexip.dev/setup".toUri()
472-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
473-
}
474-
context.startActivity(intent)
475-
}
476-
477+
477478
fun shareAdbCommand() {
478479
val context = application.applicationContext
479480
val sendIntent = Intent().apply {

app/src/main/java/dev/lexip/hecate/ui/setup/ShareExtensions.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ package dev.lexip.hecate.ui.setup
1515
import android.content.Intent
1616

1717
// Helper to share a URL via Android Sharesheet, reused by setup components.
18-
internal fun android.content.Context.shareSetupUrl(url: String) {
19-
if (url.isBlank()) return
18+
internal fun android.content.Context.shareText(
19+
text: String,
20+
title: String? = "Adaptive Theme"
21+
) {
22+
if (text.isBlank()) return
2023

2124
val sendIntent = Intent().apply {
2225
action = Intent.ACTION_SEND
23-
putExtra(Intent.EXTRA_TEXT, url)
24-
putExtra(Intent.EXTRA_TITLE, "Setup - Adaptive Theme")
26+
putExtra(Intent.EXTRA_TEXT, text)
27+
putExtra(Intent.EXTRA_TITLE, title)
2528
type = "text/plain"
2629
}
2730

0 commit comments

Comments
 (0)