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
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,14 @@ adb install document-manager-release.apk

```bash
# You need JDK 17 and Android SDK (API 34)
source scripts/setup-env.sh # or set JAVA_HOME and ANDROID_HOME manually

# On Ubuntu/Linux, run the setup script
bash scripts/setup.sh

# On macOS, run the setup script
bash scripts/setup.sh

# Then build
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
Expand Down Expand Up @@ -217,15 +223,31 @@ Yeah, there's a DICOM viewer. Built from scratch — no library. Parses the tag-

### Prerequisites

- **JDK 17** — Azul Zulu works well: `brew install --cask zulu17`
- **JDK 17** — Azul Zulu or Temurin work well
- **Android SDK** — API 34, Build Tools 34.0.0
- **A phone or emulator** — Tested on Pixel 10a (arm64)

### Environment

For **Ubuntu/Linux**:
```bash
# Run the setup script to install all dependencies
bash scripts/setup.sh

# Or manually configure environment
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export ANDROID_HOME=$HOME/android-dev-tools/android-sdk
echo "sdk.dir=$ANDROID_HOME" > local.properties
```

For **macOS**:
```bash
export JAVA_HOME=~/android-dev-tools/zulu17.54.21-ca-jdk17.0.13-macosx_x64
export ANDROID_HOME=~/android-dev-tools/android-sdk
# Run the setup script to install all dependencies
bash scripts/setup.sh

# Or manually configure environment
export JAVA_HOME=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
export ANDROID_HOME=$HOME/android-dev-tools/android-sdk
echo "sdk.dir=$ANDROID_HOME" > local.properties
```

Expand Down
20 changes: 10 additions & 10 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
abiFilters += listOf("arm64-v8a")
}
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = 2
versionName = "1.0.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand All @@ -31,12 +31,12 @@ android {

signingConfigs {
create("release") {
// For development/testing: uses debug keystore
// Replace with proper keystore before Play Store publishing
storeFile = file(System.getenv("HOME") + "/.android/debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
// Release keystore - replace with your own before Play Store publishing
// Generate with: ./scripts/release_keystore.sh
storeFile = file(System.getenv("HOME") + "/release.keystore")
storePassword = "documentmanager2024"
keyAlias = "upload_key"
keyPassword = "documentmanager2024"
}
}

Expand Down Expand Up @@ -131,8 +131,8 @@ dependencies {
// ML Kit Text Recognition v2
implementation("com.google.mlkit:text-recognition:16.0.0")

// ML Kit Document Scanner
implementation("com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1")
// ML Kit Document Scanner (stable version)
implementation("com.google.android.gms:play-services-mlkit-document-scanner:16.0.0")

// Google Drive API
implementation("com.google.api-client:google-api-client-android:2.2.0")
Expand Down
60 changes: 57 additions & 3 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,65 @@
-keep class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**

# Keep SQLCipher native libraries

# Keep Room entities
# Keep Room entities and database classes
-keep class com.app.traveldocs.data.local.** { *; }
-keep class * extends androidx.room.RoomDatabase { *; }
-keep @androidx.room.Entity class * { *; }
-keep @androidx.room.Database class * { *; }

# Keep Hilt generated components
-keep class dagger.hilt.** { *; }
-dontwarn dagger.hilt.**

# Jetpack Compose - essential for proper functioning
-keep class androidx.compose.** { *; }
-keep interface androidx.compose.** { *; }
-dontwarn androidx.compose.**

# Lifecycle components
-keep class androidx.lifecycle.** { *; }
-keepclasseswithmembers class * { @androidx.lifecycle.* <methods>; }

# Navigation components
-keep class androidx.navigation.** { *; }

# ML Kit (on-device OCR)
-keep class com.google.mlkit.** { *; }
-dontwarn com.google.mlkit.**

# Google Play Services & API Client
-keep class com.google.android.gms.** { *; }
-keep class com.google.api.** { *; }
-dontwarn com.google.android.gms.**
-dontwarn com.google.api.**

# Apache HttpClient (for Google Drive API)
-keep class org.apache.http.** { *; }
-dontwarn org.apache.http.**
-dontwarn javax.naming.**
-dontwarn org.ietf.jgss.**

# Retrofit/OkHttp (for Drive/S3 backend)
-keep class retrofit2.** { *; }
-keep class okhttp3.** { *; }

# AndroidX Core
-keep class androidx.core.** { *; }
-keep class androidx.biometric.** { *; }
-keep class androidx.security.** { *; }

# Keep native libraries
-keepclasseswithmembernames class * {
native <methods>;
}

# Keep annotations used by Hilt and other libraries
-keep @dagger.hilt.android.HiltAndroidApp class *
-keep class javax.inject.** { *; }
-keep class * extends dagger.hilt.android.internal.managers.** { *; }

# Keep generated code for Hilt
-keep class dagger.hilt.internal.** { *; }

# Keep auto-generated code for Navigation
-keep class com.app.traveldocs.di.** { *; }
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@
<!-- Audio playback service for Android Auto + system media controls -->
<service
android:name=".media.AudioPlaybackService"
android:exported="true"
tools:ignore="ExportedService">
android:exported="false">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ fun MainScreen(
DropdownMenuItem(text = { Text("About") }, onClick = { showGearMenu = false; onAbout() })
DropdownMenuItem(text = { Text("Feedback") }, onClick = { showGearMenu = false; onFeedback() })
if (com.app.traveldocs.data.local.FeatureFlags.isExperimentalEnabled(context) && com.app.traveldocs.data.local.FeatureFlags.isBackupRestoreEnabled(context)) DropdownMenuItem(text = { Text("Backup (Preview)") }, onClick = { showGearMenu = false; onBackup() })
if (BuildConfig.DEBUG || com.app.traveldocs.data.local.FeatureFlags.isExperimentalEnabled(context)) DropdownMenuItem(text = { Text("Diagnostics") }, onClick = { showGearMenu = false; onDiagnostics() })
if (com.app.traveldocs.data.local.FeatureFlags.isExperimentalEnabled(context)) DropdownMenuItem(text = { Text("Diagnostics") }, onClick = { showGearMenu = false; onDiagnostics() })
if (com.app.traveldocs.data.local.FeatureFlags.isExperimentalEnabled(context) && com.app.traveldocs.data.local.FeatureFlags.isBackupRestoreEnabled(context)) DropdownMenuItem(text = { Text("Restore (Preview)") }, onClick = { showGearMenu = false; onRestore() })
DropdownMenuItem(text = { Text("Review & Classify") }, onClick = { showGearMenu = false; onReview() })
if (com.app.traveldocs.data.local.FeatureFlags.isExperimentalEnabled(context) && com.app.traveldocs.data.local.FeatureFlags.isWifiShareEnabled(context)) DropdownMenuItem(text = { Text("WiFi Share") }, onClick = { showGearMenu = false; onWebShare() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,32 @@ fun DocumentViewerScreen(
} else { CircularProgressIndicator(Modifier.size(36.dp)) }
}
fileBytes != null && document.format == DocumentFormat.PDF -> {
val pdfInfoState = produceState<Triple<File, android.os.ParcelFileDescriptor, android.graphics.pdf.PdfRenderer>?>(null, fileBytes) { value = withContext(Dispatchers.IO) { try { val f = File.createTempFile("p", ".pdf", context.cacheDir); f.writeBytes(fileBytes!!); val fd = android.os.ParcelFileDescriptor.open(f, android.os.ParcelFileDescriptor.MODE_READ_ONLY); Triple(f, fd, android.graphics.pdf.PdfRenderer(fd)) } catch (e: Exception) { null } } }
val pdfInfoState = produceState<Triple<File, android.os.ParcelFileDescriptor, android.graphics.pdf.PdfRenderer>?>(null, fileBytes) { value = withContext(Dispatchers.IO) {
try {
val f = File.createTempFile("p", ".pdf", context.cacheDir); f.writeBytes(fileBytes!!); val fd = android.os.ParcelFileDescriptor.open(f, android.os.ParcelFileDescriptor.MODE_READ_ONLY); Triple(f, fd, android.graphics.pdf.PdfRenderer(fd))
} catch (e: Exception) {
DebugLogger.e("PDF", "PDFRenderer creation failed: ${e.message}", e); null
}
} }
val pdfInfo = pdfInfoState.value
if (pdfInfo != null) { val (_, _, renderer) = pdfInfo; val pageCount = renderer.pageCount; val pdfDispatcher = remember { Executors.newSingleThreadExecutor().asCoroutineDispatcher() }; var pdfScale by remember { mutableFloatStateOf(1f) }; var ox by remember { mutableFloatStateOf(0f) }; var oy by remember { mutableFloatStateOf(0f) }
if (pdfInfo != null) { val (pdfFile, fd, renderer) = pdfInfo; val pageCount = renderer.pageCount; val pdfDispatcher = remember { Executors.newSingleThreadExecutor().asCoroutineDispatcher() }; var pdfScale by remember { mutableFloatStateOf(1f) }; var ox by remember { mutableFloatStateOf(0f) }; var oy by remember { mutableFloatStateOf(0f) }
Box(Modifier.fillMaxSize().pointerInput(Unit) { detectTransformGestures { _, pan, zoom, _ -> pdfScale = (pdfScale * zoom).coerceIn(1f, 5f); if (pdfScale > 1f) { ox += pan.x; oy += pan.y } else { ox = 0f; oy = 0f } } }) {
LazyColumn(Modifier.fillMaxSize().background(Color.White).graphicsLayer(scaleX = pdfScale, scaleY = pdfScale, translationX = ox, translationY = oy)) {
items(pageCount) { idx -> val bmpState = produceState<Bitmap?>(null, idx) { value = withContext(pdfDispatcher) { try { val page = renderer.openPage(idx); val bmp = Bitmap.createBitmap(page.width * 2, page.height * 2, Bitmap.Config.ARGB_8888); bmp.eraseColor(android.graphics.Color.WHITE); page.render(bmp, null, null, android.graphics.pdf.PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); bmp } catch (e: Exception) { null } } }
val bmp = bmpState.value; if (bmp != null) { Image(bitmap = bmp.asImageBitmap(), contentDescription = "Page ${idx+1}", modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), contentScale = ContentScale.FillWidth) } else { Box(Modifier.fillMaxWidth().height(300.dp).background(Color(0xFFF5F5F5)), contentAlignment = Alignment.Center) { CircularProgressIndicator(Modifier.size(24.dp)) } } } } }
} else { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon(Icons.Filled.PictureAsPdf, null, tint = Color(0xFFF44336), modifier = Modifier.size(64.dp)); Text("PDF preview failed", color = Color.Gray) } }
items(pageCount) { idx ->
val bmpState = produceState<Bitmap?>(null, idx) {
value = withContext(pdfDispatcher) {
try {
val page = renderer.openPage(idx); val bmp = Bitmap.createBitmap(page.width * 2, page.height * 2, Bitmap.Config.ARGB_8888); bmp.eraseColor(android.graphics.Color.WHITE); page.render(bmp, null, null, android.graphics.pdf.PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); bmp
} catch (e: Exception) {
DebugLogger.e("PDF", "Page render failed: idx=$idx, err=${e.message}", e); null
}
}
}
val bmp = bmpState.value; if (bmp != null) { Image(bitmap = bmp.asImageBitmap(), contentDescription = "Page ${idx+1}", modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), contentScale = ContentScale.FillWidth) } else { Box(Modifier.fillMaxWidth().height(300.dp).background(Color(0xFFF5F5F5)), contentAlignment = Alignment.Center) { Column { CircularProgressIndicator(Modifier.size(24.dp)); Text("Page ${idx+1} loading", fontSize = 11.sp, color = Color.Gray) } } }
}
}
}
} else { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon(Icons.Filled.PictureAsPdf, null, tint = Color(0xFFF44336), modifier = Modifier.size(64.dp)); Spacer(Modifier.height(8.dp)); Text("PDF preview failed", fontSize = 13.sp, color = Color.Gray); Text("File may be corrupted or not a valid PDF", fontSize = 11.sp, color = Color(0xFF9E9E9E)) } }
}
fileBytes != null && (document.format == DocumentFormat.VIDEO || document.format == DocumentFormat.AUDIO) -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon(Icons.Filled.OpenInNew, null, tint = Color(0xFF2196F3), modifier = Modifier.size(64.dp)); Spacer(Modifier.height(12.dp)); Text(if (document.format == DocumentFormat.AUDIO) "Audio" else "Video", fontSize = 16.sp); Text("${fileBytes!!.size / 1024} KB", color = Color.Gray, fontSize = 13.sp); Spacer(Modifier.height(16.dp)); Button(onClick = {
val mime = if (document.format == DocumentFormat.AUDIO) "audio/mpeg" else "video/mp4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ fun SettingsScreen(onBack: () -> Unit) {
Column(modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(16.dp)) {
Text("Experimental Features", fontWeight = FontWeight.Bold, fontSize = 16.sp)
Spacer(Modifier.height(4.dp))
Text("Enable features that are in development. May be unstable.", fontSize = 12.sp, color = Color.Gray)
Text("Enable features that are in development. May be unstable.", fontSize = 12.sp)
Spacer(Modifier.height(12.dp))
Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color(0xFFF3E5F5))) {
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
ToggleRow("Enable Experimental Features", experimental) { experimental = it; FeatureFlags.setExperimental(context, it) }
if (experimental) {
Expand Down Expand Up @@ -162,7 +162,7 @@ private fun ToggleRow(label: String, checked: Boolean, onChanged: (Boolean) -> U
@Composable
private fun SubToggleRow(label: String, checked: Boolean, onChanged: (Boolean) -> Unit) {
Row(modifier = Modifier.fillMaxWidth().padding(start = 24.dp), verticalAlignment = Alignment.CenterVertically) {
Text(label, modifier = Modifier.weight(1f), fontSize = 13.sp, color = Color(0xFF616161))
Text(label, modifier = Modifier.weight(1f), fontSize = 13.sp)
Switch(checked = checked, onCheckedChange = onChanged)
}
}
Expand Down
13 changes: 2 additions & 11 deletions app/src/main/res/drawable/ic_launcher_background.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,9 @@
android:viewportWidth="108"
android:viewportHeight="108">

<!-- Deep blue gradient background -->
<!-- Professional dark blue background -->
<path
android:fillColor="#0D47A1"
android:fillColor="#1E3A8A"
android:pathData="M0,0 L108,0 L108,108 L0,108 Z"/>

<!-- Subtle lighter accent in center for depth -->
<path
android:fillColor="#1565C0"
android:pathData="M20,20 L88,20 L88,88 L20,88 Z"/>

<!-- Even lighter center area -->
<path
android:fillColor="#1976D2"
android:pathData="M30,30 L78,30 L78,78 L30,78 Z"/>
</vector>
58 changes: 11 additions & 47 deletions app/src/main/res/drawable/ic_launcher_foreground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,25 @@
android:viewportWidth="108"
android:viewportHeight="108">

<!-- Passport body -->
<!-- Document shape with rounded corners -->
<path
android:fillColor="#FFFFFF"
android:pathData="M34,28 L74,28 C76.2,28 78,29.8 78,32 L78,76 C78,78.2 76.2,80 74,80 L34,80 C31.8,80 30,78.2 30,76 L30,32 C30,29.8 31.8,28 34,28 Z"/>
android:pathData="M24,22 L84,22 C85.7,22 87,23.3 87,25 L87,83 C87,84.7 85.7,86 84,86 L24,86 C22.3,86 21,84.7 21,83 L21,25 C21,23.3 22.3,22 24,22 Z"/>

<!-- Passport spine line -->
<path
android:fillColor="#B3E5FC"
android:pathData="M30,28 L36,28 L36,80 L30,80 Z"/>

<!-- Circle emblem on passport -->
<!-- Document page lines (simplified) -->
<path
android:fillColor="#1565C0"
android:pathData="M54,46 m-10,0 a10,10 0 1,1 20,0 a10,10 0 1,1 -20,0"/>

<!-- Globe lines inside circle (horizontal) -->
<path
android:strokeColor="#B3E5FC"
android:strokeWidth="0.8"
android:fillColor="#00000000"
android:pathData="M44.5,44 L63.5,44"/>
android:pathData="M32,36 L76,36 L76,38 L32,38 Z"/>
<path
android:strokeColor="#B3E5FC"
android:strokeWidth="0.8"
android:fillColor="#00000000"
android:pathData="M44.5,48 L63.5,48"/>

<!-- Globe lines inside circle (vertical arc) -->
<path
android:strokeColor="#B3E5FC"
android:strokeWidth="0.8"
android:fillColor="#00000000"
android:pathData="M54,36 C50,40 50,52 54,56"/>
android:fillColor="#1565C0"
android:pathData="M32,46 L76,46 L76,48 L32,48 Z"/>
<path
android:strokeColor="#B3E5FC"
android:strokeWidth="0.8"
android:fillColor="#00000000"
android:pathData="M54,36 C58,40 58,52 54,56"/>
android:fillColor="#1565C0"
android:pathData="M32,56 L76,56 L76,58 L32,58 Z"/>

<!-- Text lines (representing document text) -->
<!-- Security lock icon (bottom-right, simple) -->
<path
android:fillColor="#90CAF9"
android:pathData="M40,62 L68,62 C68.6,62 69,62.4 69,63 L69,64 C69,64.6 68.6,65 68,65 L40,65 C39.4,65 39,64.6 39,64 L39,63 C39,62.4 39.4,62 40,62 Z"/>
<path
android:fillColor="#90CAF9"
android:pathData="M40,68 L60,68 C60.6,68 61,68.4 61,69 L61,70 C61,70.6 60.6,71 60,71 L40,71 C39.4,71 39,70.6 39,70 L39,69 C39,68.4 39.4,68 40,68 Z"/>
android:fillColor="#FFFFFF"
android:pathData="M66,68 L72,68 L72,72 L66,72 Z M66,73 L72,73 L72,80 L69,82 L66,80 Z M66,70 C66,69.4 66.4,69 67,69 L71,69 C71.6,69 72,69.4 72,70 L72,73 L66,73 Z"/>

<!-- Small shield/lock icon (bottom-right) representing security -->
<path
android:fillColor="#1565C0"
android:pathData="M68,72 L68,68 C68,66.3 69.3,65 71,65 C72.7,65 74,66.3 74,68 L74,72 L68,72 Z"/>
<path
android:fillColor="#FFD600"
android:pathData="M66,72 L76,72 L76,79 L71,81 L66,79 Z"/>
<path
android:fillColor="#1565C0"
android:pathData="M70,75 L72,75 L72,77 L70,77 Z"/>
</vector>
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Document Manager</string>
<string name="app_name">DocVault</string>

<!-- Main Screen -->
<string name="main_title">Travel Documents</string>
Expand Down Expand Up @@ -110,7 +110,7 @@

<!-- Consent / Onboarding -->
<string name="welcome">Welcome</string>
<string name="welcome_subtitle">Document Manager</string>
<string name="welcome_subtitle">DocVault</string>
<string name="encryption_explanation">Your documents will be encrypted with AES-256 and protected by a PIN. All data stays on this device. No cloud, no servers.</string>
<string name="create_pin">Create your PIN</string>
<string name="pin_label">PIN (4–8 digits)</string>
Expand Down
Loading
Loading