Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d9ef290
chore: remove stray scratch files from module root
jessejamesjohnston Jul 10, 2026
94efa30
refactor: replace fully-qualified inline references with imports
jessejamesjohnston Jul 10, 2026
45fbc67
refactor: introduce type-safe navigation routes
jessejamesjohnston Jul 10, 2026
ea4b86e
refactor: extract flash orchestration into FlashViewModel
jessejamesjohnston Jul 10, 2026
a6dd30a
chore: move hardcoded UI strings to resources
jessejamesjohnston Jul 10, 2026
aedb495
chore: remove leftover debug logging and fix compiler warnings
jessejamesjohnston Jul 10, 2026
75616c1
chore: add .editorconfig matching the Kotlin official style
jessejamesjohnston Jul 10, 2026
b483f0c
ci: build and lint pull requests with GitHub Actions
jessejamesjohnston Jul 10, 2026
57cd276
fix: count the channel screen as wizard step 2
jessejamesjohnston Jul 10, 2026
a189aa5
chore: extract the percent label in EraseScreen missed by the strings…
jessejamesjohnston Jul 10, 2026
68e9272
build: upgrade to current stable toolchain
jessejamesjohnston Jul 10, 2026
ff1f415
refactor: migrate off APIs deprecated by the version bump
jessejamesjohnston Jul 10, 2026
9548fa2
refactor: hold EraseScreen's status line as a resource ID
jessejamesjohnston Jul 10, 2026
fe20fc3
feat: fetch the recovery manifest once and share it
jessejamesjohnston Jul 10, 2026
6686859
feat: survive configuration changes instead of suppressing them
jessejamesjohnston Jul 10, 2026
19fd881
feat: cap wizard content at a readable width on large screens
jessejamesjohnston Jul 10, 2026
d104df3
feat: list-detail pane layout for model selection on expanded widths
jessejamesjohnston Jul 10, 2026
503570f
feat: keep the USB device list live on the drive screen
jessejamesjohnston Jul 10, 2026
44903c8
feat: drive text-field steps from a hardware keyboard
jessejamesjohnston Jul 10, 2026
f87d010
feat: adopt Material 3 Expressive theme and type scale
jessejamesjohnston Jul 10, 2026
0da1cb9
feat: wavy flash progress and morphing loading indicators
jessejamesjohnston Jul 10, 2026
ff5fc4d
feat: shape-morph success badge and calm error badge
jessejamesjohnston Jul 10, 2026
938e4cb
feat: expressive motion for wizard transitions
jessejamesjohnston Jul 10, 2026
00dc162
chore: haptic feedback across the wizard
jessejamesjohnston Jul 10, 2026
a06fd48
feat: expressive shape morphing on the primary welcome action
jessejamesjohnston Jul 10, 2026
6d7d02e
feat: digest primitives for flash verification, with unit tests
jessejamesjohnston Jul 10, 2026
622f218
fix: harden Bulk-Only Transport loops against short reads
jessejamesjohnston Jul 10, 2026
82b95ec
feat: verify flashed images end to end
jessejamesjohnston Jul 10, 2026
57cb17c
feat: make erase real — zero the drive's partition structures
jessejamesjohnston Jul 10, 2026
86d8d6e
fix: hide the erase menu action while an operation is running
jessejamesjohnston Jul 10, 2026
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
20 changes: 20 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Kotlin official code style (matches kotlin.code.style=official in gradle.properties)
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{kt,kts}]
indent_size = 4
indent_style = space
max_line_length = 120

[*.{xml,yml,yaml}]
indent_size = 4
indent_style = space

[*.md]
trim_trailing_whitespace = false
28 changes: 28 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Android CI

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: android
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- name: Build and lint
run: ./gradlew assembleDebug lint
- name: Upload lint report
if: failure()
uses: actions/upload-artifact@v4
with:
name: lint-report
path: android/app/build/reports/lint-results-debug.html
49 changes: 29 additions & 20 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}

android {
namespace = "com.google.chrome.recovery"
compileSdk = 36
compileSdk = 37

defaultConfig {
applicationId = "com.google.chrome.recovery"
Expand All @@ -30,18 +30,12 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
Expand All @@ -51,28 +45,43 @@ android {

dependencies {

implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2024.04.01"))
implementation("androidx.core:core-ktx:1.19.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.11.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0")
implementation("androidx.activity:activity-compose:1.13.0")
implementation(platform("androidx.compose:compose-bom:2026.06.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")

// Pinned past the BOM's 1.4.0: every Material 3 Expressive API (wavy progress,
// LoadingIndicator, MaterialExpressiveTheme, MaterialShapes) lives on the
// 1.5.0-alpha track — Expressive was removed from the 1.4 line at beta01.
implementation("androidx.compose.material3:material3:1.5.0-alpha23")
// material3 stopped depending on material-icons transitively in newer BOMs;
// the icons artifacts are frozen at 1.7.x and are no longer BOM-managed.
implementation("androidx.compose.material:material-icons-core:1.7.8")

// Navigation
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("androidx.navigation:navigation-compose:2.9.8")

// Adaptive layouts (window size classes + list-detail pane scaffold).
// material3-adaptive-navigation-suite is deliberately absent: it exists for
// apps with top-level destinations (bar/rail/drawer); a linear wizard has none.
implementation("androidx.compose.material3.adaptive:adaptive:1.2.0")
implementation("androidx.compose.material3.adaptive:adaptive-layout:1.2.0")
implementation("androidx.compose.material3.adaptive:adaptive-navigation:1.2.0")

// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")

// JSON Parsing (Gson or Kotlinx Serialization, let's use Gson for simplicity)
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.google.code.gson:gson:2.14.0")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.04.01"))
androidTestImplementation(platform("androidx.compose:compose-bom:2026.06.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
Expand Down
4 changes: 1 addition & 3 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
android:icon="@mipmap/ic_launcher"
android:label="Book Recovery"
android:supportsRtl="true"
android:theme="@style/Theme.ChromebookRecovery"
android:usesCleartextTraffic="true">
android:theme="@style/Theme.ChromebookRecovery">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:label="Book Recovery"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:theme="@style/Theme.ChromebookRecovery">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.google.chrome.recovery.data
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.InputStreamReader
import java.net.HttpURLConnection
Expand All @@ -17,18 +19,42 @@ import java.net.URL
* for the zipped `.bin` payloads.
*
* It aggregates both standard ChromeOS models and CloudReady (ChromeOS Flex) models.
*
* The manifest is fetched once per process and cached: several wizard screens
* (identify, model selection, channel selection, and the expanded-width detail
* pane) all need the same ~2,500-entry list, and previously each of them
* re-downloaded it. Access the repository through [RecoveryRepository.instance].
*/
class RecoveryRepository {
class RecoveryRepository private constructor() {

companion object {
/** Process-wide instance so every screen shares one cached manifest. */
val instance = RecoveryRepository()
}

private val RECOVERY_URL = "https://dl.google.com/dl/edgedl/chromeos/recovery/recovery2.json"
private val CLOUDREADY_URL = "https://dl.google.com/dl/edgedl/chromeos/recovery/cloudready_recovery2.json"

suspend fun fetchRecoveryImages(): List<RecoveryImage> = withContext(Dispatchers.IO) {
private val mutex = Mutex()
private var cached: List<RecoveryImage>? = null

suspend fun fetchRecoveryImages(): List<RecoveryImage> = mutex.withLock {
cached?.let { return@withLock it }
val images = fetchFromNetwork()
// An empty result means the fetch failed; don't cache it, so the next
// screen gets another chance once connectivity returns.
if (images.isNotEmpty()) {
cached = images
}
images
}

private suspend fun fetchFromNetwork(): List<RecoveryImage> = withContext(Dispatchers.IO) {
val images = mutableListOf<RecoveryImage>()
try {
val listType = object : TypeToken<List<RecoveryImage>>() {}.type
val gson = Gson()

// Fetch normal ChromeOS recovery
val url = URL(RECOVERY_URL)
val connection = url.openConnection() as HttpURLConnection
Expand Down
Loading