Skip to content
Draft
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
32 changes: 32 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Android
on:
workflow_dispatch:
pull_request:
paths: ["android/**", ".github/workflows/android.yml"]
jobs:
build:
runs-on: ubuntu-latest
defaults: { run: { working-directory: android } }
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '17' }
- uses: android-actions/setup-android@v3
- run: ./gradlew --version
- run: ./gradlew projects
- run: ./gradlew verifyArchitecture verifyAndroidPrivacy dependencyVerification
- run: ./gradlew test lint :apps:documents:assembleDebug :apps:documents:assembleRelease
- run: git diff --exit-code -- android/**/gradle.lockfile android/settings-gradle.lockfile android/gradle/verification-metadata.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: android-engineering-apks
path: android/apps/documents/build/outputs/apk/**/*.apk
- uses: actions/upload-artifact@v4
if: always()
with:
name: android-test-and-lint-reports
path: |
android/**/build/reports/tests/**
android/**/build/reports/lint-results-*.html
5 changes: 5 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
gradle-*.zip
gradle-*/
.gradle/
local.properties
**/build/
37 changes: 37 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# PersianToolbox Android

Native Kotlin/Compose foundation for «جعبه‌ابزار اسناد فارسی».

The workspace targets JDK 17, minSdk 26, compileSdk/targetSdk 36, and uses only
Google and Maven Central repositories. The MVP manifest intentionally has no
`INTERNET` or broad-storage permission. Documents and OCR content remain local.

## Windows build

From `android/`:

```powershell
.\gradlew.bat --version
.\gradlew.bat projects
.\gradlew.bat test lint :apps:documents:assembleDebug :apps:documents:assembleRelease
.\gradlew.bat dependencyVerification
```

Engineering APKs are emitted under `apps/documents/build/outputs/apk/`. No
production signing key is included or required for the unsigned release gate.

The current foundation includes the Persian RTL shell, stable domain models,
processing interfaces, privacy checks, and module boundaries. CameraX/OpenCV/
PdfBox/Tesseract risk spikes remain bounded adapter work and must not be
described as completed product functionality until their fixture and release
evidence exists.

## Current host evidence

The repository includes the official Gradle 8.13 wrapper. On the Phase 1
workstation, wrapper validation succeeds with JDK 17, but Android Gradle Plugin
resolution is blocked because every tested artifact URL in the official Google
Maven repository returns HTTP 404. This is an environment/repository-access
blocker, not evidence of a successful Android build. Run the commands above on
a host where Google Maven returns Android artifacts before relying on generated
APKs or test results.
9 changes: 9 additions & 0 deletions android/apps/documents/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins { alias(libs.plugins.android.application); alias(libs.plugins.kotlin.android); alias(libs.plugins.compose.compiler) }
android { namespace = "ir.persiantoolbox.documents"; compileSdk = 36
defaultConfig { applicationId = "ir.persiantoolbox.documents"; minSdk = 26; targetSdk = 36; versionCode = 1; versionName = "1.0.0"; testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
kotlinOptions { jvmTarget = "17" }
buildTypes { release { isMinifyEnabled = true; isShrinkResources = true; proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } }
buildFeatures { compose = true }
}
dependencies { implementation(project(":core:designsystem")); implementation(project(":feature:home")); implementation(libs.androidx.core.ktx); implementation(libs.androidx.activity.compose); implementation(platform(libs.androidx.compose.bom)); implementation(libs.androidx.compose.ui); implementation(libs.androidx.compose.material3) }
256 changes: 256 additions & 0 deletions android/apps/documents/gradle.lockfile

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions android/apps/documents/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Engine-specific rules are added only with a verified adapter.
22 changes: 22 additions & 0 deletions android/apps/documents/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
android:label="جعبه‌ابزار اسناد فارسی"
android:supportsRtl="true"
android:theme="@style/Theme.PersianToolbox">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.files"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/document_paths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ir.persiantoolbox.documents

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.runtime.CompositionLocalProvider
import ir.persiantoolbox.designsystem.PersianToolboxTheme
import ir.persiantoolbox.feature.home.HomeScreen

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); setContent { App() } }
}

@Composable private fun App() {
val dark = remember { mutableStateOf(false) }
PersianToolboxTheme(dark.value) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
HomeScreen { action -> if (action.route == "settings") dark.value = !dark.value }
}
}
}
1 change: 1 addition & 0 deletions android/apps/documents/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<resources><style name="Theme.PersianToolbox" parent="android:style/Theme.Material.Light.NoActionBar"/></resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup disableIfNoEncryptionCapabilities="true">
<exclude domain="file" path="." />
<exclude domain="database" path="." />
</cloud-backup>
<device-transfer>
<exclude domain="file" path="." />
<exclude domain="database" path="." />
</device-transfer>
</data-extraction-rules>
5 changes: 5 additions & 0 deletions android/apps/documents/src/main/res/xml/document_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="shared_documents" path="documents/" />
<cache-path name="shared_temporary_documents" path="share/" />
</paths>
63 changes: 63 additions & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import org.gradle.api.artifacts.ProjectDependency

plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.compose.compiler) apply false
}

allprojects {
dependencyLocking {
lockAllConfigurations()
}
}

val projectDependencyEdges = allprojects.flatMap { project ->
project.configurations.flatMap { configuration ->
configuration.dependencies.withType<ProjectDependency>().map { dependency ->
Triple(project.path, configuration.name, dependency.path)
}
}
}

tasks.register("verifyArchitecture") {
doLast {
projectDependencyEdges.forEach { (projectPath, _, target) ->
when {
projectPath.startsWith(":core") && (target.startsWith(":feature") || target.startsWith(":apps")) -> error("Forbidden dependency: $projectPath -> $target")
projectPath.startsWith(":processing") && (target.startsWith(":feature") || target.startsWith(":apps")) -> error("Forbidden dependency: $projectPath -> $target")
projectPath.startsWith(":feature") && target.startsWith(":feature") && target != projectPath -> error("Feature-to-feature dependency: $projectPath -> $target")
}
}
}
}

val manifestFiles = fileTree(rootDir) { include("**/src/main/AndroidManifest.xml") }.files.toList()
val manifestContents = manifestFiles.associateWith { it.readText() }
val gradlePolicyFiles = fileTree(rootDir) {
include("**/*.gradle.kts")
include("**/*.toml")
exclude("build.gradle.kts")
}.files.toList()
val gradlePolicyContents = gradlePolicyFiles.associateWith { it.readText() }

tasks.register("verifyAndroidPrivacy") {
doLast {
manifestContents.forEach { (manifest, text) ->
check(!text.contains("android.permission.INTERNET")) { "INTERNET permission found in $manifest" }
check(!text.contains("MANAGE_EXTERNAL_STORAGE")) { "Broad storage permission found in $manifest" }
check(!text.contains("READ_EXTERNAL_STORAGE")) { "Legacy storage permission found in $manifest" }
check(!text.contains("WRITE_EXTERNAL_STORAGE")) { "Legacy storage permission found in $manifest" }
}
val appManifest = manifestContents.getValue(file("apps/documents/src/main/AndroidManifest.xml"))
check(appManifest.contains("android:allowBackup=\"false\"")) { "Document backup must be disabled" }
check(appManifest.contains("androidx.core.content.FileProvider")) { "Secure FileProvider is required" }
check(appManifest.contains("android:exported=\"false\"")) { "FileProvider must not be exported" }
gradlePolicyContents.forEach { (file, text) ->
check(!text.contains("jitpack.io")) { "JitPack found in $file" }
}
}
}
tasks.register("dependencyVerification") { dependsOn("verifyArchitecture", "verifyAndroidPrivacy") }
2 changes: 2 additions & 0 deletions android/core/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
plugins { alias(libs.plugins.kotlin.jvm) }
dependencies { api(project(":core:model")); api(libs.kotlinx.coroutines.core); testImplementation(libs.junit) }
45 changes: 45 additions & 0 deletions android/core/common/gradle.lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.github.ben-manes.caffeine:caffeine:2.9.3=swiftExportClasspathResolvable
com.google.errorprone:error_prone_annotations:2.28.0=swiftExportClasspathResolvable
io.github.java-diff-utils:java-diff-utils:4.12=kotlinInternalAbiValidation
io.opentelemetry:opentelemetry-api:1.41.0=swiftExportClasspathResolvable
io.opentelemetry:opentelemetry-context:1.41.0=swiftExportClasspathResolvable
junit:junit:4.13.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.bouncycastle:bcpg-jdk18on:1.80=kotlinBouncyCastleConfiguration
org.bouncycastle:bcpkix-jdk18on:1.80=kotlinBouncyCastleConfiguration
org.bouncycastle:bcprov-jdk18on:1.80.2=kotlinBouncyCastleConfiguration
org.bouncycastle:bcutil-jdk18on:1.80.2=kotlinBouncyCastleConfiguration
org.checkerframework:checker-qual:3.43.0=swiftExportClasspathResolvable
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:abi-tools-api:2.2.21=kotlinInternalAbiValidation
org.jetbrains.kotlin:abi-tools:2.2.21=kotlinInternalAbiValidation
org.jetbrains.kotlin:kotlin-build-tools-api:2.2.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.2.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
org.jetbrains.kotlin:kotlin-compiler-runner:2.2.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.2.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:2.2.21=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-metadata-jvm:2.2.21=kotlinInternalAbiValidation
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
org.jetbrains.kotlin:kotlin-script-runtime:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
org.jetbrains.kotlin:kotlin-scripting-common:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,runtimeClasspath,swiftExportClasspathResolvable,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:swift-export-embeddable:2.2.21=swiftExportClasspathResolvable
org.jetbrains.kotlinx:atomicfu:0.23.1=apiDependenciesMetadata,implementationDependenciesMetadata,testImplementationDependenciesMetadata
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=swiftExportClasspathResolvable
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=swiftExportClasspathResolvable
org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=swiftExportClasspathResolvable
org.jetbrains:annotations:13.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
org.jetbrains:annotations:23.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=annotationProcessor,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDefExtensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ir.persiantoolbox.common

import ir.persiantoolbox.model.*
import kotlinx.coroutines.flow.Flow

data class DetectionResult(val corners: Quadrilateral?)
interface ProgressReporter { val progress: Flow<ProcessingProgress>; suspend fun report(progress: ProcessingProgress) }
interface ImageProcessor { suspend fun detectDocument(input: ImageRef): DetectionResult; suspend fun transform(input: ImageRef, crop: Quadrilateral, filter: PageFilter): ImageRef }
interface PdfEngine { suspend fun create(pages: List<ImageRef>, output: FileRef, options: PdfOptions): PdfResult; suspend fun merge(inputs: List<FileRef>, output: FileRef): PdfResult; suspend fun split(input: FileRef, ranges: List<PageRange>, outputDir: DirectoryRef): List<PdfResult>; suspend fun compress(input: FileRef, output: FileRef, quality: CompressionQuality): PdfResult }
interface OcrEngine { suspend fun recognize(input: ImageRef, languages: Set<OcrLanguage>): OcrText }
1 change: 1 addition & 0 deletions android/core/database/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
plugins { alias(libs.plugins.kotlin.jvm) }
Loading
Loading