Initial commit: Foto Dup Finder 1.0.1
An Android duplicate-photo finder. Pure-Kotlin core (SHA-256 + dHash hashing, exact and perceptual union-find grouping, three sensitivity presets) with MediaStore access, a persistent SQLite hash index and a tiered ScanEngine behind a Jetpack Compose UI. No INTERNET permission, no ads, no analytics, no account. Ships versionCode 2 / versionName 1.0.1, carrying the scan-stall hardening (BoundedCall breaker) and verified against a 5,363-photo device library. 80 JVM unit tests, lint clean. Release paperwork is in docs/ (Play checklist, store listing, edge-case testing), the upload-ready Play graphics and screenshots in store-assets/, and the generators for those assets in tools/.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Debug-only test hook — merged into the *debug* manifest alone, never the release one.
|
||||
|
||||
It exists because the test device (MIUI) denies `adb shell input` (INJECT_EVENTS), so a
|
||||
scan cannot be started by tapping the button and `pm grant` is refused as well. The
|
||||
receiver lets a scan be driven over adb instead:
|
||||
|
||||
adb shell am broadcast -a de.softwareapp_hb.fotodup.finder.debug.START_SCAN \
|
||||
-n de.softwareapp_hb.fotodup.finder.debug/de.softwareapp_hb.fotodup.finder.debug.DebugScanTrigger
|
||||
|
||||
Verified absent from the release manifest by the permission/receiver audit in
|
||||
docs/PLAY_RELEASE_CHECKLIST.md § 5.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!--
|
||||
Debug-only. This device (MIUI) refuses every shell permission grant — `pm grant` and
|
||||
`install -g` both fail with "Neither user 2000 nor current process has
|
||||
GRANT_RUNTIME_PERMISSIONS" — and `adb shell input` is denied, so the normal
|
||||
"Grant access" tap is impossible.
|
||||
|
||||
"All files access" *can* be granted from adb (`appops set <pkg>
|
||||
MANAGE_EXTERNAL_STORAGE allow`), and MediaProvider honours it, which is enough to
|
||||
exercise the real scan over the device's photo library:
|
||||
|
||||
adb shell appops set de.softwareapp_hb.fotodup.finder.debug MANAGE_EXTERNAL_STORAGE allow
|
||||
|
||||
The release manifest must never contain this permission — re-check it with the
|
||||
audit command in docs/PLAY_RELEASE_CHECKLIST.md § 5.
|
||||
-->
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<application>
|
||||
<receiver
|
||||
android:name=".debug.DebugScanTrigger"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.softwareapp_hb.fotodup.finder.debug
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import de.softwareapp_hb.fotodup.finder.FotoDupApplication
|
||||
import de.softwareapp_hb.fotodup.finder.core.DuplicatePolicy
|
||||
import de.softwareapp_hb.fotodup.finder.data.AppPermissions
|
||||
|
||||
/**
|
||||
* Debug-only hook that lets a device scan be driven over `adb` — see `src/debug/AndroidManifest.xml`.
|
||||
*
|
||||
* Not part of the app: it lives in the debug source set, so it is absent from the release build.
|
||||
* It reports everything it does through [TAG] so `adb logcat -s FotoDupDebug` shows the outcome.
|
||||
*/
|
||||
class DebugScanTrigger : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val app = context.applicationContext as? FotoDupApplication ?: return
|
||||
val controller = app.scanController
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_START_SCAN -> {
|
||||
val policy = when (intent.getStringExtra(EXTRA_POLICY)?.lowercase()) {
|
||||
"exact" -> DuplicatePolicy.EXACT_ONLY
|
||||
"aggressive" -> DuplicatePolicy.AGGRESSIVE
|
||||
else -> DuplicatePolicy.BALANCED
|
||||
}
|
||||
Log.i(TAG, "START_SCAN policy=$policy access=${AppPermissions.accessState(context)}")
|
||||
controller.start(policy)
|
||||
report(controller)
|
||||
}
|
||||
|
||||
ACTION_REPORT_STATE -> report(controller)
|
||||
}
|
||||
}
|
||||
|
||||
private fun report(controller: de.softwareapp_hb.fotodup.finder.domain.ScanController) {
|
||||
val state = controller.state.value
|
||||
val result = state.result
|
||||
Log.i(
|
||||
TAG,
|
||||
"STATE scanning=${state.scanning} phase=${state.phase} " +
|
||||
"progress=${state.done}/${state.total} error=${state.error} " +
|
||||
"photos=${result?.photoCount} groups=${result?.groupCount} " +
|
||||
"unreadable=${result?.unreadableCount} timings=${result?.timings}",
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START_SCAN = "de.softwareapp_hb.fotodup.finder.debug.START_SCAN"
|
||||
const val ACTION_REPORT_STATE = "de.softwareapp_hb.fotodup.finder.debug.REPORT_STATE"
|
||||
const val EXTRA_POLICY = "policy"
|
||||
const val TAG = "FotoDupDebug"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user