Skip to content

Latest commit

 

History

History
328 lines (258 loc) · 12.5 KB

File metadata and controls

328 lines (258 loc) · 12.5 KB

Files+ (AI File Manager)

Scope: Full‑fledged AI file manager on Android; AI file classifier/organiser on iOS. All agenting runs on‑device with Koog, using Gemini Nano when available and direct model APIs (e.g., Gemini via user API key) otherwise. No server backend required for MVP. Files+ is a full‑scale, AI‑powered File Manager: browse, search, tag, move, copy, rename, and delete files across shared storage with on‑device AI assistance.

Owner: Rivu Chakraborty Codebase root package: com.mobriostudio.filesplus
Modules: :app-android, :app-ios, :shared


1) Product Goals & Non‑Goals

Goals

  1. Organise & clean up user storage intelligently.
  2. On‑device first: classify locally; optionally escalate to cloud model APIs only when uncertainty remains and the user permits.
  3. Privacy‑preserving: never upload full files by default; only bounded snippets or thumbnails on demand.
  4. Observable agents: live Agent Trace during scans for demo & debugging.
  5. Android: end‑to‑end file management (scan, classify, dedupe, move/delete via SAF/MediaStore, search).
    iOS: classify/organise with review flows (deletion/move through user‑picked URLs).

Non‑Goals (MVP)

  • Clearing 3rd‑party app caches or tampering with sandboxed data.
  • Full text search across entire device.
  • Cloud backup/sync.

2) Top‑Level Architecture

KMP App (Android/iOS)
  ├─ :shared (Kotlin Multiplatform)
  │    ├─ On‑device Koog Micro‑Agents (Planner/Tagging/Dedupe/Policy)
  │    ├─ On‑device PreClassifier (Gemini Nano / Gemma via MediaPipe)
  │    ├─ File scanning abstraction + models (SQLDelight)
  │    ├─ Heuristics (filename, path, mime, EXIF tokens)
  │    ├─ Dedupe helpers (sha256, pHash, Hamming distance)
  │    └─ Feature flags & Privacy policy
  ├─ :app-android (Compose, SAF/MediaStore, ML Kit OCR, AICore/Nano)
  └─ :app-ios (SwiftUI wrapper → Compose MPP view, UIDocumentPicker, Vision OCR)

2.1 Execution Modes (All on‑device)

  • Local‑first (default): On‑device Koog orchestrates local tools (heuristics, ML Kit OCR, pHash). For model reasoning, it calls Gemini Nano (if available) or MediaPipe Gemma.
  • Cloud‑assist (optional): If confidence remains low and privacy allows, Koog calls model REST APIs directly from device with user‑provided API keys (redacted snippets only).

3) Core Use Cases

  1. Smart Categories: Documents, Receipts, IDs, Bank Statements, Photos, Videos, Audio, APKs, Archives, Manuals, Tickets, Warranty, Coursework, Other.
  2. Duplicate & Similar Finder: exact dupes (sha256) and near‑dup images (pHash cluster). Keep‑rule suggestions (highest resolution/newest).
  3. Cleanup Cards: Large files, Old downloads, Messaging app residuals (Statuses/Forwarded/Stickers), Orphaned APK/ZIP.
  4. Explainability: Show why a tag was chosen (signals: "GSTIN", "Invoice #", "₹", EXIF, folder hints).
  5. Privacy Modes: On = no network (Gemini Nano/Gemma only); Cloud‑assist = minimal redacted snippets to model API; never upload full files.

4) Data & Models (Shared)

SQLDelight Schema

-- module :shared/src/commonMain/sqldelight/filesplus.db.sq
CREATE TABLE files (
  id TEXT PRIMARY KEY,
  uri TEXT NOT NULL,
  name TEXT NOT NULL,
  mime TEXT NOT NULL,
  size INTEGER NOT NULL,
  addedAt INTEGER,
  lastAccessedAt INTEGER,
  sha256 TEXT,
  pHash TEXT,
  width INTEGER,
  height INTEGER,
  durationMs INTEGER
);

CREATE TABLE tags (
  fileId TEXT NOT NULL,
  tag TEXT NOT NULL,
  confidence REAL NOT NULL,
  evidenceJson TEXT NOT NULL,
  PRIMARY KEY (fileId, tag)
);

CREATE TABLE clusters (
  clusterId TEXT NOT NULL,
  fileId TEXT NOT NULL,
  score REAL NOT NULL,
  PRIMARY KEY (clusterId, fileId)
);

Shared Models

@Serializable data class FileDescriptor(
  val id: String,
  val name: String,
  val mime: String,
  val size: Long,
  val addedAt: Long? = null,
  val lastAccessedAt: Long? = null,
  val sha256: String? = null,
  val pHash: String? = null,
  val width: Int? = null,
  val height: Int? = null,
  val durationMs: Long? = null,
  val exif: Map<String, String>? = null,
  val sampleText: String? = null
)

@Serializable data class TagScore(val name: String, val score: Double)
@Serializable data class PreClassifyResult(
  val tags: List<TagScore>,
  val uncertainty: Double,
  val reasons: List<String> = emptyList()
)

sealed class RouteDecision {
  data class LocalAccept(val tags: List<TagScore>) : RouteDecision()
  data class AskServer(
    val redactedSnippet: String?,
    val signals: Map<String, String>
  ) : RouteDecision()
}

5) On‑Device Pre‑Classifier

Decision Policy

  • If top score ≥ 0.88 and uncertainty ≤ 0.15 → accept locally.
  • 0.60–0.88 → call cloud model API (Gemini via user API key) with redacted snippet + signals.
  • < 0.60 → run additional local heuristics/OCR or ask the user; if allowed, call cloud model API with minimal redacted snippet.

Redaction

  • Replace Aadhaar/PAN/IFSC/phone/email with placeholders before any prompt.

Gemini Nano (preferred) — Android only

suspend fun tryGeminiNano(prompt: String): PreClassifyResult? {
  val generator = OnDeviceGenAi.getTextGeneratorOrNull(appContext) ?: return null
  val output = generator.generate(
    OnDeviceTextPrompt(prompt),
    OnDeviceTextOptions.Builder().setMaxOutputTokens(96).setTemperature(0f).build()
  )
  return parseClassifierJson(output.text)
}

Fallback: MediaPipe GenAI (Gemma 2B / Recurrent)

suspend fun gemmaClassify(prompt: String): PreClassifyResult {
  val response = genai.generateText(prompt, maxTokens = 96, temperature = 0f)
  return parseClassifierJson(response.text)
}

Compact Prompt (closed set)

System:
Classify into tags from:
[invoice, receipt, bank-statement, resume, recipe, id-document, ticket, warranty, manual, coursework, apk, archive, photo, video, audio, other].
Return strict JSON:
{"tags":[{"name":"<tag>","score":0.00}],"uncertainty":0.00,"reasons":["..."]}

User:
filename="invoice_march.pdf" mime="application/pdf"
hints=["GSTIN","Total ₹"]
text="GSTIN 29ABCDE1234F1Z5  Invoice #0021  Total ₹ 1,240"

6) On‑Device Agents (Koog)

  • LocalScanAgent: gather descriptors; compute sha256/pHash/EXIF; per‑file plan.
  • LocalTaggingAgent: MIME → (OCR/TextExtract if available) → Heuristics → (Nano/Gemma) → fuse.
  • LocalDuplicateAgent: pHash clustering; keep‑rule proposal.
  • LocalPolicyAgent: inline PII redaction and privacy enforcement before any network call.

6.1 On‑device Koog Runner (skeleton)

class LocalKoogEnvironment(
  private val nano: NanoClient?, // Gemini Nano client (Android)
  private val gemma: GemmaClient?,
  private val ocr: OcrClient,
  private val heur: Heuristics,
  private val policy: Policy,
  private val cloud: CloudModels? // direct REST when privacy allows
) {
  suspend fun classify(fd: FileDescriptor, privacy: Boolean): PreClassifyResult {
    val hints = heur.buildHints(fd)
    val text = ocr.snippet(fd)?.let { policy.redact(it) }
    val prompt = buildPrompt(fd, hints, text)
    val local = nano?.classify(prompt) ?: gemma?.classify(prompt)
    if (local != null && local.tags.firstOrNull()?.score ?: 0.0 >= 0.88 && local.uncertainty <= 0.15) return local
    if (!privacy) return cloud?.classify(prompt) ?: (local ?: PreClassifyResult(emptyList(),1.0))
    return local ?: PreClassifyResult(emptyList(),1.0)
  }
}

7) On‑Device Tools (no server)

interface Tool { val name: String; suspend fun invoke(args: Map<String, Any?>): Map<String, Any?> }
class MimeDetectTool: Tool { /* mime */ }
class LocalOCRTool: Tool { /* ML Kit / Vision */ }
class MediaProbeTool: Tool { /* width/height/duration/bitrate */ }
class PHashTool: Tool { /* compute + cluster with Hamming distance */ }
class HeuristicsTool: Tool { /* filename/ext/dir regexes + ₹, IFSC, GSTIN tokens */ }
class PolicySanitizeTool: Tool { /* redact */ }

8) Security & API Keys (on device)

  • Store keys in Android Keystore/EncryptedSharedPreferences and iOS Keychain.
  • Never hardcode or log keys. Provide provider allowlist (e.g., generativelanguage.googleapis.com).
  • Strict redaction before any cloud call; bounded context (≤4 KB); temperature=0; maxTokens≤96.

9) Android Implementation

Permissions & Storage

  • READ_MEDIA_IMAGES/VIDEO/AUDIO (API ≥ 33) or READ_EXTERNAL_STORAGE (≤ 32).
  • SAF for delete/move; MediaStore for media URIs.

MANAGE_EXTERNAL_STORAGE — Permission Management Plan (Android 11+)

When we use it

  • Files+ is a full‑scale file manager (browse, search, move, copy, rename, delete across shared storage, SD/OTG). For these core workflows on Android 11+ where SAF/MediaStore cannot deliver a reasonable UX (e.g., bulk operations across many folders, root of SD/OTG), we request MANAGE_EXTERNAL_STORAGE.
  • Default mode uses MediaStore + SAF. We only ask for All files access when the user enables Full File Manager Mode or starts an action that requires broad access.

Play policy alignment

  • Request MANAGE_EXTERNAL_STORAGE only for core file‑manager functionality and provide a fallback via SAF when not granted.
  • Ship a clear in‑app education screen explaining why we need it, with a one‑tap deep link to system settings.
  • Complete the Google Play All files access declaration form and document our core use cases.

Manifest (flavor‑gated)

<!-- In AndroidManifest.xml for the `fullManager` flavor -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

Runtime flow

fun hasAllFilesAccess(): Boolean =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Environment.isExternalStorageManager() else true

fun requestAllFilesAccess(context: Context) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    val pkg = Uri.parse("package:" + context.packageName)
    val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, pkg)
    try { context.startActivity(intent) } catch (e: ActivityNotFoundException) {
      context.startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
    }
  }
}
  • Basic read access and full storage management both require MANAGE_EXTERNAL_STORAGE permission; the app will not function without it. Users must grant this permission during onboarding to continue.
  • Show a concise rationale dialog before deep‑linking to settings; summarize what access means and how to revoke it.
  • Provide a Privacy & Access screen that shows current status and a toggle to use SAF‑only mode.

Testing & debugging

  • Enable for testing via adb shell appops set --uid <package> MANAGE_EXTERNAL_STORAGE allow.
  • Verify that bulk delete/move works across shared storage roots; verify downgrade to SAF works when permission is revoked.

Notes

  • requestLegacyExternalStorage is ignored when targeting API 30+; don’t rely on it.
  • Even with All files access, /Android/data/ of other apps remains inaccessible; design flows accordingly.

Scanning

suspend fun scanMediaStore(): List<FileDescriptor> { /* query images/video/audio */ }
suspend fun scanDownloadsSAF(): List<FileDescriptor> { /* via DocumentFile tree */ }

Hashes & pHash

fun sha256(uri: Uri): String { /* stream → digest */ }
fun pHash(bitmap: Bitmap): Long {
  // 1) resize 32x32, 2) grayscale, 3) DCT, 4) take top-left 8x8, 5) median threshold
}
fun hamming(a: Long, b: Long) = java.lang.Long.bitCount(a xor b)

OCR (ML Kit)

suspend fun ocrSnippet(context: Context, uri: Uri, maxChars: Int = 2048): String { /* VisionTextProcessor */ }

Pre‑Classifier Router

suspend fun routeFor(file: FileDescriptor): RouteDecision {
  val hints = buildHints(file)
  val prompt = buildPrompt(file, hints)
  val nano = tryGeminiNano(prompt)
  val local = nano ?: gemmaClassify(prompt)
  val calibrated = plattScale(local)
  return if (calibrated.tags.firstOrNull()?.score ?: 0.0 >= 0.88 && calibrated.uncertainty <= 0.15)
    RouteDecision.LocalAccept(calibrated.tags)
  else RouteDecision.AskServer(redact(file.sampleText), buildSignals(file, hints, calibrated))
}

Compose UI