Add files via upload

This commit is contained in:
GLdashboard
2025-07-19 20:16:52 -05:00
committed by GitHub
parent 39997ee490
commit 4ed252c20c
43 changed files with 2262 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.nooktrmnl"
compileSdk = 34
defaultConfig {
applicationId = "com.example.nooktrmnl"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.1.1"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
}
dependencies {
implementation(libs.androidx.appcompat)
val composeBomVersion = "2023.10.01"
implementation(platform("androidx.compose:compose-bom:$composeBomVersion"))
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("io.coil-kt:coil-compose:2.5.0")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
implementation("androidx.work:work-runtime-ktx:2.9.0")
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.example.nooktrmnl
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.nooktrmnl", appContext.packageName)
}
}
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Nooktrmnl"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Nooktrmnl">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:exported="true"
android:label="@string/settings"
android:theme="@style/Theme.Nooktrmnl">
</activity>
<receiver android:name=".UpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.example.nooktrmnl.ACTION_UPDATE" />
</intent-filter>
</receiver>
<receiver android:name=".HeadphoneReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.HEADSET_PLUG" />
</intent-filter>
</receiver>
</application>
</manifest>
@@ -0,0 +1,210 @@
package com.example.nooktrmnl
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
class DisplayUpdateWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
companion object {
const val KEY_REFRESH_RATE = "refresh_rate"
const val ACTION_NETWORK_COMPLETE = "com.example.nooktrmnl.ACTION_NETWORK_COMPLETE"
const val ACTION_SCREEN_REFRESH_COMPLETE = "com.example.nooktrmnl.ACTION_SCREEN_REFRESH_COMPLETE"
private const val NETWORK_TIMEOUT_MS = 30000L
private const val SCREEN_REFRESH_TIMEOUT_MS = 15000L
private const val LOG_FILE_SIZE_LIMIT = 1024 * 1024
}
private var networkComplete = false
private var screenRefreshComplete = false
private var actualRefreshRate = 60
private fun logActivity(message: String) {
try {
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
val logEntry = "$timestamp - DisplayUpdateWorker: $message\n"
val logFile = File(applicationContext.getExternalFilesDir(null), "update_activity.log")
if (logFile.exists() && logFile.length() > LOG_FILE_SIZE_LIMIT) {
val content = logFile.readText()
val lines = content.lines()
val keepLines = lines.takeLast(lines.size / 2)
logFile.writeText(keepLines.joinToString("\n") + "\n")
}
logFile.appendText(logEntry)
Log.d("DisplayUpdateWorker", message)
} catch (e: Exception) {
Log.e("DisplayUpdateWorker", "Failed to write activity log", e)
}
}
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
logActivity("Starting update work")
val powerManager = PowerManagerNook(applicationContext)
logActivity("Ensuring WiFi connection")
var wifiAttempts = 0
var wifiSuccess = false
while (!wifiSuccess && wifiAttempts < 3) {
wifiSuccess = powerManager.ensureWifiOn()
if (!wifiSuccess) {
val backoffDelay = (wifiAttempts + 1) * 2000L
logActivity("WiFi enable attempt ${wifiAttempts + 1} failed, waiting ${backoffDelay}ms")
delay(backoffDelay)
wifiAttempts++
}
logActivity("WiFi attempt $wifiAttempts status: $wifiSuccess")
}
if (!wifiSuccess) {
logActivity("Failed to enable WiFi after 3 attempts")
powerManager.goToSleep()
return@withContext Result.retry()
}
logActivity("WiFi connection established successfully")
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
ACTION_NETWORK_COMPLETE -> {
logActivity("Network completion notification received")
val newRefreshRate = intent.getIntExtra("refresh_rate", 60)
val source = intent.getStringExtra("from_source") ?: "unknown"
logActivity("Received refresh rate: ${newRefreshRate}s from source: $source")
if (newRefreshRate > 0) {
actualRefreshRate = newRefreshRate
logActivity("Updated refresh rate to ${actualRefreshRate}s")
}
networkComplete = true
}
ACTION_SCREEN_REFRESH_COMPLETE -> {
logActivity("Screen refresh completion notification received")
screenRefreshComplete = true
}
}
}
}
val filter = IntentFilter().apply {
addAction(ACTION_NETWORK_COMPLETE)
addAction(ACTION_SCREEN_REFRESH_COMPLETE)
}
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(receiver, filter)
try {
val refreshRate = inputData.getInt(KEY_REFRESH_RATE, 60)
val intent = Intent(MainActivity.ACTION_DISPLAY_UPDATE).apply {
putExtra("trigger_update", true)
}
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
logActivity("Waiting for network requests to complete...")
val networkSuccess = withTimeoutOrNull(NETWORK_TIMEOUT_MS) {
while (!networkComplete) {
delay(500)
if (System.currentTimeMillis() % 2000 < 500) {
logActivity("Still waiting for network completion...")
}
}
true
} ?: false
if (networkSuccess) {
logActivity("Network requests completed successfully")
logActivity("Waiting for screen refresh to complete...")
val screenSuccess = withTimeoutOrNull(SCREEN_REFRESH_TIMEOUT_MS) {
while (!screenRefreshComplete) {
delay(500)
if (System.currentTimeMillis() % 1000 < 500) {
logActivity("Still waiting for screen refresh...")
}
}
true
} ?: false
if (screenSuccess) {
logActivity("Screen refresh completed successfully")
} else {
logActivity("Screen refresh timed out after ${SCREEN_REFRESH_TIMEOUT_MS}ms - proceeding anyway")
}
} else {
logActivity("Network requests timed out after ${NETWORK_TIMEOUT_MS}ms")
actualRefreshRate = refreshRate
logActivity("Using fallback refresh rate: ${actualRefreshRate}s")
}
logActivity("Scheduling next update")
scheduleNextUpdate(applicationContext, actualRefreshRate)
} finally {
LocalBroadcastManager.getInstance(applicationContext).unregisterReceiver(receiver)
}
logActivity("Going back to sleep")
powerManager.goToSleep()
logActivity("Update work completed successfully")
Result.success()
} catch (e: Exception) {
logActivity("Error during update: ${e.message}")
try {
val powerManager = PowerManagerNook(applicationContext)
powerManager.goToSleep()
} catch (sleepError: Exception) {
logActivity("Failed to go to sleep after error: ${sleepError.message}")
}
Result.retry()
}
}
@SuppressLint("ScheduleExactAlarm")
private fun scheduleNextUpdate(context: Context, refreshRate: Int) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val updateIntent = Intent(context, UpdateReceiver::class.java).apply {
action = MainActivity.ACTION_UPDATE
}
val pendingIntent = PendingIntent.getBroadcast(
context,
MainActivity.UPDATE_REQUEST_CODE,
updateIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val nextUpdateTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(refreshRate.toLong())
alarmManager.setAlarmClock(
AlarmManager.AlarmClockInfo(nextUpdateTime, pendingIntent),
pendingIntent
)
logActivity("Next update scheduled for: ${Date(nextUpdateTime)} (${refreshRate}s from now)")
}
}
@@ -0,0 +1,58 @@
package com.example.nooktrmnl
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class HeadphoneReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_HEADSET_PLUG) {
val state = intent.getIntExtra("state", -1)
Log.d("HeadphoneReceiver", "Headset state changed: $state")
if (state == 1) {
val pendingResult = goAsync()
CoroutineScope(Dispatchers.Default).launch {
try {
Log.d("HeadphoneReceiver", "Attempting wake on headset connect")
val powerManager = PowerManagerNook(context)
powerManager.wakeForUpdate()
val workRequest = OneTimeWorkRequestBuilder<DisplayUpdateWorker>()
.setInputData(
Data.Builder()
.putInt(DisplayUpdateWorker.KEY_REFRESH_RATE, MainActivity.refreshRate)
.build()
)
.addTag("com.example.nooktrmnl.DisplayUpdateWorker")
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"displayUpdate",
ExistingWorkPolicy.REPLACE,
workRequest
)
Log.d("HeadphoneReceiver", "DisplayUpdateWorker enqueued successfully")
Log.d("HeadphoneReceiver", "Headphone-triggered update completed - alarms left intact")
} catch (e: Exception) {
Log.e("HeadphoneReceiver", "Error in HeadphoneReceiver", e)
} finally {
pendingResult.finish()
}
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
package com.example.nooktrmnl
import android.content.ContentResolver
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import kotlinx.coroutines.delay
class PowerManagerNook(private val context: Context) {
private val contentResolver: ContentResolver = context.contentResolver
private val handler = Handler(Looper.getMainLooper())
private val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private var wakeLock: PowerManager.WakeLock? = null
companion object {
private const val POWER_ENHANCE_ENABLE = "power_enhance_enable"
private const val LOG_TAG = "NookTerminalPower"
private const val POWER_STATE_DELAY = 200L
private const val SLEEP_CHECK_DELAY = 50L
private const val NETWORK_CHECK_INTERVAL = 500L
private const val MAX_NETWORK_ATTEMPTS = 30
private const val NETWORK_STABILIZE_DELAY = 1000L
}
suspend fun ensureWifiOn(): Boolean {
if (isNetworkConnected()) {
Log.d(LOG_TAG, "Already connected to network")
return true
}
if (!wifiManager.isWifiEnabled) {
Log.d(LOG_TAG, "WiFi is off, enabling")
wifiManager.isWifiEnabled = true
}
return waitForNetwork()
}
private suspend fun waitForNetwork(): Boolean {
var attempts = 0
while (attempts < MAX_NETWORK_ATTEMPTS) {
if (isNetworkConnected()) {
Log.d(LOG_TAG, "Network connected after ${attempts * NETWORK_CHECK_INTERVAL}ms")
delay(NETWORK_STABILIZE_DELAY)
return true
}
delay(NETWORK_CHECK_INTERVAL)
attempts++
Log.d(LOG_TAG, "Waiting for network... Attempt $attempts")
}
Log.e(LOG_TAG, "Failed to establish network connection")
return false
}
private fun isNetworkConnected(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
capabilities?.let {
it.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
it.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} ?: false
} else {
@Suppress("DEPRECATION")
connectivityManager.activeNetworkInfo?.isConnected == true
}
}
suspend fun wakeForUpdate() {
try {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK or
PowerManager.ACQUIRE_CAUSES_WAKEUP or
PowerManager.ON_AFTER_RELEASE,
"NookTerminal::FullWakeLock"
).apply {
acquire(10 * 60 * 1000L) // 10 minute timeout
}
val before = getPowerEnhanceState()
setPowerEnhanceState(0)
val after = getPowerEnhanceState()
Log.d(LOG_TAG, "Wake - power_enhance_enable changed from $before to $after")
delay(POWER_STATE_DELAY * 2)
val actualPowerState = getPowerEnhanceState()
Log.d(LOG_TAG, "Power enhance state after delay: $actualPowerState")
} catch (e: Exception) {
Log.e(LOG_TAG, "Error waking device", e)
}
}
suspend fun goToSleep() {
try {
try {
if (wifiManager.isWifiEnabled) {
Log.d(LOG_TAG, "Disabling WiFi for sleep")
wifiManager.isWifiEnabled = false
}
} catch (e: Exception) {
Log.e(LOG_TAG, "Error disabling WiFi", e)
}
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
Log.d(LOG_TAG, "Wake lock released")
}
}
wakeLock = null
} catch (e: Exception) {
Log.e(LOG_TAG, "Error releasing wake lock", e)
}
setPowerEnhanceState(0)
delay(POWER_STATE_DELAY)
setPowerEnhanceState(1)
Log.d(LOG_TAG, "Sleep command sent - checking state")
delay(SLEEP_CHECK_DELAY)
val state = getPowerEnhanceState()
Log.d(LOG_TAG, "Sleep state after delay: $state")
} catch (e: Exception) {
Log.e(LOG_TAG, "Error putting device to sleep", e)
}
}
private fun getPowerEnhanceState(): Int {
return Settings.System.getInt(contentResolver, POWER_ENHANCE_ENABLE, 0)
}
private fun setPowerEnhanceState(state: Int) {
Settings.System.putInt(contentResolver, POWER_ENHANCE_ENABLE, state)
}
}
@@ -0,0 +1,209 @@
package com.example.nooktrmnl
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SettingsScreen()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen() {
val context = LocalContext.current
var baseUrl by remember { mutableStateOf(MainActivity.BASE_URL) }
var macAddress by remember { mutableStateOf(MainActivity.MAC_ADDRESS) }
var apiKey by remember { mutableStateOf(MainActivity.API_KEY) }
var userAgent by remember { mutableStateOf(MainActivity.USER_AGENT) }
var showDebugInfo by remember { mutableStateOf(MainActivity.SHOW_DEBUG_INFO) }
val timeoutOptions = listOf(
"2 minutes" to 120000,
"15 days" to 1296000000,
"30 days" to 2592000000L.toInt(),
"Never" to Int.MAX_VALUE
)
val currentTimeout = remember {
try {
Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 60000)
} catch (e: Exception) {
60000
}
}
val initialSelection = timeoutOptions.indexOfFirst { it.second == currentTimeout }.let { index ->
if (index >= 0) index else 0
}
var selectedTimeoutIndex by remember { mutableIntStateOf(initialSelection) }
var timeoutDropdownExpanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
TextField(
value = baseUrl,
onValueChange = { baseUrl = it },
label = { Text("Base URL") },
modifier = Modifier.fillMaxWidth()
)
TextField(
value = macAddress,
onValueChange = { macAddress = it },
label = { Text("MAC Address") },
modifier = Modifier.fillMaxWidth()
)
TextField(
value = apiKey,
onValueChange = { apiKey = it },
label = { Text("API Key") },
modifier = Modifier.fillMaxWidth()
)
TextField(
value = userAgent,
onValueChange = { userAgent = it },
label = { Text("User Agent") },
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Screen Timeout",
modifier = Modifier.padding(bottom = 4.dp)
)
ExposedDropdownMenuBox(
expanded = timeoutDropdownExpanded,
onExpandedChange = { timeoutDropdownExpanded = !timeoutDropdownExpanded },
modifier = Modifier.fillMaxWidth()
) {
TextField(
value = timeoutOptions[selectedTimeoutIndex].first,
onValueChange = { },
readOnly = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = timeoutDropdownExpanded)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = timeoutDropdownExpanded,
onDismissRequest = { timeoutDropdownExpanded = false }
) {
timeoutOptions.forEachIndexed { index, (label, _) ->
DropdownMenuItem(
text = { Text(label) },
onClick = {
selectedTimeoutIndex = index
timeoutDropdownExpanded = false
}
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text("Show Debug Info")
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = showDebugInfo,
onCheckedChange = { showDebugInfo = it }
)
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
saveSettings(
context = context,
baseUrl = baseUrl,
macAddress = macAddress,
apiKey = apiKey,
userAgent = userAgent,
showDebugInfo = showDebugInfo,
screenTimeoutMs = timeoutOptions[selectedTimeoutIndex].second
)
(context as? ComponentActivity)?.finish()
},
modifier = Modifier.align(Alignment.End),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.White,
contentColor = Color.Black
),
border = BorderStroke(1.dp, Color.Black)
) {
Text("Save")
}
}
}
fun saveSettings(
context: Context,
baseUrl: String,
macAddress: String,
apiKey: String,
userAgent: String,
showDebugInfo: Boolean,
screenTimeoutMs: Int
) {
val sharedPreferences: SharedPreferences = context.getSharedPreferences("AppSettings", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("BASE_URL", baseUrl)
editor.putString("MAC_ADDRESS", macAddress)
editor.putString("API_KEY", apiKey)
editor.putString("USER_AGENT", userAgent)
editor.putBoolean("SHOW_DEBUG_INFO", showDebugInfo)
editor.putInt("SCREEN_TIMEOUT_MS", screenTimeoutMs)
editor.apply()
MainActivity.BASE_URL = baseUrl
MainActivity.MAC_ADDRESS = macAddress
MainActivity.API_KEY = apiKey
MainActivity.USER_AGENT = userAgent
MainActivity.SHOW_DEBUG_INFO = showDebugInfo
try {
Settings.System.putInt(
context.contentResolver,
Settings.System.SCREEN_OFF_TIMEOUT,
screenTimeoutMs
)
Log.d("SettingsActivity", "Screen timeout set to ${screenTimeoutMs}ms")
} catch (e: Exception) {
Log.e("SettingsActivity", "Failed to set screen timeout", e)
}
}
@@ -0,0 +1,100 @@
package com.example.nooktrmnl
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.util.Log
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object UpdateManager {
private const val WORK_NAME = "displayUpdate"
private const val WORK_TAG = "com.example.nooktrmnl.DisplayUpdateWorker"
fun triggerUpdate(context: Context, triggerSource: String) {
val pendingResult = if (context is BroadcastReceiver) {
context.goAsync()
} else null
CoroutineScope(Dispatchers.Default).launch {
try {
Log.d("UpdateManager", "Update triggered by: $triggerSource")
logPowerState(context)
if (triggerSource == "headphone_primary") {
cancelScheduledUpdates(context)
Log.d("UpdateManager", "Cancelled any pending alarms")
val powerManager = PowerManagerNook(context)
powerManager.wakeForUpdate()
WorkManager.getInstance(context).cancelAllWorkByTag(WORK_TAG)
val workRequest = OneTimeWorkRequestBuilder<DisplayUpdateWorker>()
.setInputData(
Data.Builder()
.putInt(DisplayUpdateWorker.KEY_REFRESH_RATE, MainActivity.refreshRate)
.build()
)
.addTag(WORK_TAG)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
WORK_NAME,
ExistingWorkPolicy.REPLACE,
workRequest
)
Log.d("UpdateManager", "DisplayUpdateWorker enqueued via headphone trigger")
} else if (triggerSource == "alarm_scheduler") {
Log.d("UpdateManager", "Alarm fired - but relying on headphone event for actual wake")
}
} catch (e: Exception) {
Log.e("UpdateManager", "Error in update trigger ($triggerSource)", e)
} finally {
pendingResult?.finish()
}
}
}
private fun logPowerState(context: Context) {
try {
val powerState = Settings.System.getInt(
context.contentResolver,
"power_enhance_enable",
-1
)
Log.d("UpdateManager", "Power state: $powerState")
} catch (e: Exception) {
Log.e("UpdateManager", "Error checking power state: ${e.message}")
}
}
private fun cancelScheduledUpdates(context: Context) {
try {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getBroadcast(
context,
MainActivity.UPDATE_REQUEST_CODE,
Intent(context, UpdateReceiver::class.java).apply {
action = MainActivity.ACTION_UPDATE
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
} catch (e: Exception) {
Log.e("UpdateManager", "Error cancelling alarms", e)
}
}
}
@@ -0,0 +1,51 @@
package com.example.nooktrmnl
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class UpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == MainActivity.ACTION_UPDATE) {
val pendingResult = goAsync()
CoroutineScope(Dispatchers.Default).launch {
try {
Log.d("UpdateReceiver", "Alarm triggered - ensuring update happens")
val powerManager = PowerManagerNook(context)
powerManager.wakeForUpdate()
val workRequest = OneTimeWorkRequestBuilder<DisplayUpdateWorker>()
.setInputData(
Data.Builder()
.putInt(DisplayUpdateWorker.KEY_REFRESH_RATE, MainActivity.refreshRate)
.build()
)
.addTag("com.example.nooktrmnl.DisplayUpdateWorker")
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"displayUpdate",
ExistingWorkPolicy.REPLACE,
workRequest
)
Log.d("UpdateReceiver", "DisplayUpdateWorker enqueued successfully")
} catch (e: Exception) {
Log.e("UpdateReceiver", "Error in UpdateReceiver", e)
} finally {
pendingResult.finish()
}
}
}
}
}
@@ -0,0 +1,11 @@
package com.example.nooktrmnl.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package com.example.nooktrmnl.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun NooktrmnlTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package com.example.nooktrmnl.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

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