ONE GLANCE.
ZERO STEPS.
:: NATIVE NOTHING OS 3.0 HOME SCREEN WIDGET
Checking your OpenRouter balance used to mean unlocking your device, opening a browser tab, logging in, and navigating to Settings → Credits. Three redundant steps for a number that belongs on your home screen.
Engineered with classic Android RemoteViews and hardware-backed EncryptedSharedPreferences. Pure AMOLED black, VT323 dot-matrix typography, and zero third-party telemetry.
VIRTUAL NOTHING PHONE (2a) CONTAINER
Inspect the widget inside an authentic Nothing OS 3.0 environment. Switch widget formats (2×1, 2×2, 4×2), simulate token burn across top LLMs, test inverted alert logic, and monitor the Glyph LED strip.
Crisp white flash on sync • Crimson pulse on budget threshold alert
Tap the widget in the phone frame to open simulated MainActivity
Widget Size & Layout
[ REMOTE_VIEWS RESIZABLE ]Real-Time Token Burn Engine
Trigger live deductions against simulated OpenRouter models to observe state transitions.
MainActivity In-App Settings
Account Mode monitors remaining credits (total_credits − total_usage). Red indicator dot triggers when balance falls below threshold.
ENGINEERED FOR STABILITY, NOT HYPE.
Every technical decision was made deliberately. No unnecessary Jetpack Compose overhead, zero third-party telemetry, and absolute hardware-level security.
Classic RemoteViews, Not Jetpack Glance
Google heavily evangelizes Jetpack Glance for modern Compose-driven widgets. However, in AGP 8.5 and Kotlin 1.9+, the Glance compiler plugin generates unpredictable crashes on OEM launchers—specifically across Nothing OS 2.5 and 3.0.
Classic RemoteViews and standard XML layouts have been refined across 15 years of Android releases. They boot with 0ms compiler penalty, never freeze launcher threads, and guarantee pixel-perfect Nothing OS rendering.
Hardware Keystore AES-256 GCM
Your OpenRouter API token grants direct financial access. We persist it using EncryptedSharedPreferences backed by the Android Keystore.
Keys are sealed inside your device's Trusted Execution Environment (TEE) or StrongBox chip. Even a rooted device cannot extract keys in plaintext.
Constraint-Aware WorkManager
Rather than draining battery with high-frequency alarms or persistent foreground services, background refreshes run through WorkManager every 45 minutes.
Refreshes require NetworkType.CONNECTED constraints and honor Android Doze mode, consuming < 0.02% battery over a 24-hour cycle.
Zero Proxies. Zero Middlemen.
The widget uses OkHttp to speak directly with openrouter.ai/api/v1 over TLS 1.3.
There are no intermediary proxy servers, no Google Analytics, no crashlytics beacons, and no proprietary backend. Your API key never touches any server besides OpenRouter.
Inverted Threshold Semantics
Depending on whether you monitor total account balance or single key usage, alert semantics automatically invert:
-
Account Mode: Alert when
balance < threshold -
Key Mode: Alert when
usage > threshold
FOUR STAGES. ZERO REDUNDANCY.
Native Android execution pipeline, from Keystore secret validation to launcher RemoteViews dispatch.
MainActivity
User enters API key. Validates via /credits. Stores AES-256 encrypted preferences in Keystore.
RefreshWorker
WorkManager periodic job fires every 45 min on active Wi-Fi/Cellular. OkHttp fetches fresh balances asynchronously.
WidgetUpdater
Evaluates mode + threshold. Formats VT323 dot-matrix string. Wires PendingIntents for tap-to-configure and manual sync.
Launcher Surface
AppWidgetManager receives RemoteViews bundle and renders directly onto Nothing Launcher grid without waking app process.
// File: app/src/main/java/com/nothingwidget/openrouter/widget/WidgetUpdater.kt
package com.nothingwidget.openrouter.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import com.nothingwidget.openrouter.R
import com.nothingwidget.openrouter.data.Prefs
object WidgetUpdater {
fun update(context: Context, mgr: AppWidgetManager, id: Int) {
val prefs = Prefs(context)
val views = RemoteViews(context.packageName, R.layout.widget_openrouter)
val balance = prefs.cachedBalance
val label = prefs.cachedLabel ?: "OPENROUTER"
val threshold = prefs.threshold
val mode = prefs.displayMode // "ACCOUNT" or "KEY"
// Inverted alert semantics
val isAlert = when (mode) {
"ACCOUNT" -> balance < threshold // Alert when balance is low
"KEY" -> balance > threshold // Alert when single-key usage exceeds quota
else -> false
}
// Bind formatted VT323 dot-matrix balance & alert indicator
views.setTextViewText(R.id.txtBalance, String.format("$%.2f", balance))
views.setImageViewResource(
R.id.imgStatusDot,
if (isAlert) R.drawable.dot_red else R.drawable.dot_white
)
// PendingIntent: Tap widget opens MainActivity
val clickIntent = Intent(context, MainActivity::class.java)
val pi = PendingIntent.getActivity(context, 0, clickIntent, PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.widgetRoot, pi)
mgr.updateAppWidget(id, views)
}
}
// File: app/src/main/java/com/nothingwidget/openrouter/data/OpenRouterApi.kt
package com.nothingwidget.openrouter.data
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.util.concurrent.TimeUnit
data class CreditsResult(val balance: Float, val usage: Float, val total: Float)
object OpenRouterApi {
private const val BASE = "https://openrouter.ai/api/v1"
private val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.callTimeout(25, TimeUnit.SECONDS)
.build()
/** GET /credits — direct fetch with zero telemetry */
suspend fun fetchCredits(apiKey: String): CreditsResult = withContext(Dispatchers.IO) {
require(apiKey.isNotBlank()) { "API key missing" }
val request = Request.Builder()
.url("$BASE/credits")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("User-Agent", "NothingOpenRouterWidget/2.0.1")
.get()
.build()
client.newCall(request).execute().use { response ->
val body = response.body?.string().orEmpty()
val json = JSONObject(body).getJSONObject("data")
val total = json.getDouble("total_credits").toFloat()
val usage = json.getDouble("total_usage").toFloat()
CreditsResult(balance = total - usage, usage = usage, total = total)
}
}
}
// File: app/src/main/java/com/nothingwidget/openrouter/work/RefreshWorker.kt
package com.nothingwidget.openrouter.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.nothingwidget.openrouter.data.OpenRouterApi
import com.nothingwidget.openrouter.data.Prefs
import com.nothingwidget.openrouter.widget.WidgetUpdater
class RefreshWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val prefs = Prefs(applicationContext)
val key = prefs.apiKey
if (key.isBlank()) return Result.failure()
return try {
val res = OpenRouterApi.fetchCredits(key)
prefs.cachedBalance = res.balance
prefs.cachedUsage = res.usage
prefs.lastUpdate = System.currentTimeMillis()
prefs.lastError = null
WidgetUpdater.updateAll(applicationContext)
Result.success()
} catch (e: Exception) {
prefs.lastError = e.message
// Retain last known value on transient network failures
WidgetUpdater.updateAll(applicationContext)
Result.retry()
}
}
}
// File: app/src/main/java/com/nothingwidget/openrouter/data/Prefs.kt
package com.nothingwidget.openrouter.data
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class Prefs(context: Context) {
// Hardware-backed Android Keystore MasterKey
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sp = EncryptedSharedPreferences.create(
context,
"openrouter_secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
var apiKey: String
get() = sp.getString("user_api_key", "").orEmpty()
set(value) = sp.edit().putString("user_api_key", value).apply()
var threshold: Float
get() = sp.getFloat("threshold", 2.0f)
set(value) = sp.edit().putFloat("threshold", value).apply()
}
• Architectural Tradeoff Evaluation
| Architectural Dilemma | Selected Implementation | Rejected Alternative | Technical Justification |
|---|---|---|---|
| Widget UI Framework | RemoteViews (XML) | Jetpack Glance | Glance compiler produces runtime crashes on Nothing OS 3.0 / AGP 8.5. RemoteViews provides 100% OEM stability. |
| Authentication Flow | Manual API Key Paste | OAuth 2.0 PKCE | OpenRouter OAuth rejects custom redirect schemes (openrouterwidget://). Manual key works unconditionally. |
| Secret Persistence | EncryptedSharedPreferences | Jetpack DataStore | DataStore lacks an official Keystore-backed encryption module. Keystore AES-256 SIV is strictly required. |
| Background Scheduler | WorkManager (45m) | AlarmManager / Service | OEM skins throttle background intervals below 30m. 45m guarantees reliable execution with < 0.02% battery drain. |
CAPTURED ON NOTHING PHONE (2a)
Real hardware captures running Nothing OS 3.0 (Android 14). Authentic dot-matrix typography and color-coded alert thresholds.
Configuration Screen
Paste API key and test connection
Post-Setup Preferences
Live balance, threshold & modes
Account Balance (2×2)
Healthy credit state on Nothing OS
Single Key Alert (2×2)
Over-budget state with Nothing Red LED
GET IT ON YOUR PHONE
Two hassle-free paths: download the pre-compiled debug APK generated automatically by GitHub Actions CI, or compile locally with Gradle.
Download Pre-Built APK
Clean, verifiable debug APK compiled directly from source on every git push via GitHub Actions clean Ubuntu runners. No Android Studio required.
Build From Source
Clone the repository and compile using JDK 17 and Android SDK 34 command line tools or Android Studio Iguana/Jellyfish.
git clone https://github.com/mirconegri/openrouter-widget.git
cd openrouter-widget
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
• First-Time Launch Checklist
Sideload APK
Transfer app-debug.apk to your device. Tap Install and enable Install unknown apps if prompted.
Paste API Key
Open OpenRouter Widget. Paste your sk-or-v1-… key. Tap VERIFICA E SALVA to encrypt into Keystore.
Place Widget
Return to home screen. Long-press empty space → Widgets → OpenRouter. Place and resize to 2×1, 2×2, or 4×2.
WHAT IT DOES NOT DO — ON PURPOSE
Deliberate scope boundaries, upstream OpenRouter API constraints, and battery protection trade-offs.
OpenRouter restricts its /workspaces and provisioning endpoints exclusively to Enterprise and Business tiers. On standard individual accounts, that API key does not exist.
OpenRouter's OAuth 2.0 implementation refuses custom URI schemes (openrouterwidget://) as valid redirect targets. A complete PKCE flow was built and dead-ends at the browser step. Manual key paste is 100% reliable.
Although WorkManager allows a 15-minute periodic schedule, modern Android OS and Nothing OS power management throttles jobs under 30 minutes when idle. The 45-minute interval reliably runs without silent system cancellation. Manual refresh is always instantaneous.
Multiple widget instances can be added to your home screen, but they currently share the same active configuration. Per-widget independent key binding is actively being developed for v2.1.