Integrating ProofMode in Simple Camera Android

Ngenge Senior
4 min readMar 9, 2023

--

Photo by Austin Distel on Unsplash

ProofMode has arguably been one of the most exciting projects I have worked on as an Android developer.

ProofMode’s purpose has been to enable any individual to generate proof of images and videos they take using their phones. This proof can be shared with an entity such as a lawyer in court when need be. Though there has been much advancement in the Android and iOS apps, one of our main goals has been to enable other third-party camera app developers to have this power of proof generation in their own apps without necessarily having to get all of ProofMode’s code.

The birth of libProofMode Android

After successfully pulling proof generation into a library tagged libProofMode Android, the next step was to integrate it into a third-party app thus Simple Camera was chosen and we succeeded in the integration. There were of course a few bugs that had to be resolved such as the app failing to do a recording when the shutter sound setting is turned on.

On the technical side of things, we choose to use Android’s WorkManager APIs so that the proof generation is done in the background without affecting the user experience. Thus when a photo or video is captured and saved, the proof generation worker is triggered to generate proof at once. This proof can be shared later as a Zip file with others.

Integrating libProofMode into Your Own App

There is a sample that can be found at the following link. The sample explains how to add the dependencies to your own app and how to generate the proof.

How Does libProofMode Android Generate Proof?

Proof generation is done using a media URI. Given a media URI, a unique media hash is generated and thus used to create the directory to store the proof for this media item. In Simple Camera, we used Android’s WorkManager.

class GenerateProofWorker(
private val ctx: Context,
workParams: WorkerParameters
) : Worker(ctx.applicationContext, workParams) {
override fun doWork(): Result {
val imageOrVideoUriString = inputData.getString(ProofModeUtils.MEDIA_KEY)
val hash = ProofMode.generateProof(ctx, Uri.parse(imageOrVideoUriString))
if (hash.isNotEmpty()) {
return Result.success(ProofModeUtils.createData(ProofModeUtils.MEDIA_HASH, hash))
}
return Result.failure()
}
}

The createData function takes a key and value and uses them to create a Data object which will serve as input for our ListenableWorker.

object ProofModeUtils {

//..... other codes
fun createData(key:String, value:Any?) :Data {
val builder = Data.Builder()
value?.let {
builder.putString(key,value.toString())
}
return builder.build()
}

}

For a proof generation in Simple Camera, the proof is generated instantly when an image is captured or a video is captured and saved to disk. MainActivity implements CameraXPreviewListener that has many functions and one of them is onMediaSaved when a media item is captured and saved. This is the proper place to trigger proof generation. Since this is something that should be done only once, we use a OneTimeRequest.

class MainActivity:SimpleActivity(),
PhotoProcessor.MediaSavedListener,
CameraXPreviewListener {

fun onMediaSaved(uri:Uri) {


val proofGenerationBuilder = OneTimeWorkRequestBuilder<GenerateProofWorker>()
val proofData = ProofModeUtils.createData(ProofModeUtils.MEDIA_KEY,uri)
proofGenerationBuilder.setInputData(proofData)
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiresBatteryNotLow(true)
.build()
proofGenerationBuilder.setConstraints(constraints)
workManager.beginUniqueWork(uri.toString(),ExistingWorkPolicy.REPLACE,proofGenerationBuilder.build()).enqueue()
workManager.getWorkInfosForUniqueWorkLiveData(uri.toString())
.observe(this) {
val workInfoData = it[0]
if (workInfoData.state == WorkInfo.State.SUCCEEDED) {
val hash = workInfoData.outputData.getString(ProofModeUtils.MEDIA_HASH)
val newIntent = Intent().apply {
data = uri
putExtra(ProofModeUtils.MEDIA_HASH,hash)
setClass(this@MainActivity,LastMediaPreviewActivity::class.java)

}
startActivity(newIntent)
}

}



}

}

In onMediaSaved we create work constraints(proof generation should only occur if the battery is not low and the storage is not low). Using the unique work ID, when the generation succeeds, we pass the Uri to anintent and navigate to LatMediaPreviewActivity.

Sharing the Proof Zip

In LastMediaPreviewActivity, when the user clicks the share icon, we create a zip containing the proof files and then trigger a share action for you to share the proof zip to any app that you chose.Here is the code snippet for sharing.

private fun shareProofFiles() {
fab_share_proof.setOnClickListener {
Toast.makeText(this, hash, Toast.LENGTH_SHORT).show()
try {
val dir = ProofMode.getProofDir(this,hash)
val zip = ProofModeUtils.makeProofZip(dir,this)
ProofModeUtils.shareZipFile(this,zip)
}catch (ex:Exception) {
ex.printStackTrace()
}

}

}

The call to ProofMode.getProofDir uses the proof hash to identify the directory where the generated proof is stored on your phone’s disk, then the call to ProofModeUtils.makeProofZip uses the directory and creates a zip file of all files in the folder, ProofModeUtils.shareZipFile is the code for the share action.

object ProofModeUtils {


fun makeProofZip(proofDirPath: File,context: Context): File {
val outputZipFile = File(context.filesDir, proofDirPath.name + ".zip")
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
proofDirPath.walkTopDown().forEach { file ->
val zipFileName = file.absolutePath.removePrefix(proofDirPath.absolutePath).removePrefix("/")
val entry = ZipEntry( "$zipFileName${(if (file.isDirectory) "/" else "" )}")
zos.putNextEntry(entry)
if (file.isFile) {
file.inputStream().copyTo(zos)
}
}
val keyEntry = ZipEntry("pubkey.asc");
zos.putNextEntry(keyEntry);
val publicKey = ProofMode.getPublicKeyString(context)
zos.write(publicKey.toByteArray())

}

return outputZipFile
}

fun shareZipFile(context: Context, zipFile: File) {
val authority = "${BuildConfig.APPLICATION_ID}.provider"
val uri = FileProvider.getUriForFile(context, authority, zipFile)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "application/zip"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(Intent.createChooser(shareIntent, "Share Proof Zip"))
}

}

Note that the shareZipFile function uses FileProvider for the secure sharing of files using content:// Uri .

What Next?

After this successful integration, we are not done. We want to take proof generation to other media formats especially audio. Thus those with audio recorder apps will be able to use ProofMode in their own apps for proof generation as well. Watch this space for more updates.

--

--