Android Attachments App


Overview

This tutorial enables you to build a fully functioning attachment sharing app in Android using Kotlin. The app will allow you to upload photos which sync across devices, showing one of the key features of Ditto SDK.

The emulator will not show any data sync because neither Bluetooth or the necessary network system is available to allow emulators to find each other or another physical device. Be sure to run the finished app on real devices!

For best results, have WiFi or LAN enabled. Bluetooth is not best for sharing attachments and used solely will limit the performance of this app.


Create Android Studio Project

This guide is based on Android Studio 4.1 and Kotlin 1.4

To get started, in Android Studio create a kotlin project that uses a recyclerview to display a list of images

Android requires requesting permission to use Bluetooth Low Energy and P2P Wifi, open the AndroidManifest.xml and add the following:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

Install Ditto SDK

To install Ditto SDK, we need to add it as a dependency in the build.gradle script for the app, as well as ensuring that we have the relevant Java compatibility set:


dependencies {
    // ...
    implementation "live.ditto:dittosynckit:1.0.0-alpha7"
}

android {
    // ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

Be sure to Sync Project with Gradle Files after you add Ditto SDK as a dependency. Click the elephant icon with the blue arrow in the top right to manually trigger if it doesn't prompt.

At this point, you have the basic project in place! Now we need to start to build the UI elements.


Set up an instance of Ditto

Since we’ll be using Ditto in multiple classes throughout the app, let’s set up a companion object to be shared. Create a file named DittoHandler.kt and add the following.

import live.ditto.DittoSyncKit

class DittoHandler {
    companion object {
       var ditto: DittoSyncKit? = null
   }
}

Run Sync Project with Gradle Files if it doesn't automatically. The file should look like this now:


In your MainActivity.kt add the following code to set the properties and start Ditto SDK:

class MainActivity : ... {

    private var liveQuery: DittoLiveQuery? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //UI related code

        //Initialize DittoSyncKit
        try {
           val androidDependencies = DefaultAndroidDittoSyncKitDependencies(applicationContext)
           DittoHandler.ditto = DittoSyncKit(
               androidDependencies,
               DittoIdentity.Development("<APP NAME>")
           )
           DittoHandler.ditto?.setAccessLicense("<INSERT ACCESS LICENSE>")
        } catch (e: Exception) {
           Log.e(e.message, e.localizedMessage)
        }
    
        DittoHandler.ditto?.start()
    }
}

The important things to note is that you need an access license to use Ditto. If you do not have one yet, reach out and we can supply one. To enable background synchronization, we need to call start() which allows you to control when synchronization occurs. For this application we want it to run the entire time the app is in use.


Create Pic Class

In this application we want to be able to insert attachments, as well as observe insertions from other devices. With that, we’ll create a new class called Pic.kt. This class will be a helper class for all of our Ditto SDK operations with attachments.

import live.ditto.DittoAttachmentToken
import live.ditto.DittoDocument

class Pic(document: DittoDocument){
    var id: String
    var imageAttachmentToken: DittoAttachmentToken? = null
    var mimetype: String

    init {
        id = document.id
        imageAttachmentToken = document["image"].attachmentToken
        mimetype = document["mimeType"].string ?: "image/jpeg"
    }

    ...
}

A DittoAttachmentToken serves as a token for a specific attachment that you can use to fetch in the future


Create Helper Functions

Next we’ll add a companion object to the Pic class and create our first method, allowing us to insert documents consisting of images into our pic collection.

companion object {

    fun insertPicture(context: Context, file: File) {
        val f = File(context.filesDir, "${UUID.randomUUID()}.jpg")
        file.copyTo(f)
        val store = DittoHandler.ditto?.store
        val attachment = store?.collection("pics")?.newAttachment(f.absolutePath)
        store?.collection("pics")?.insert(
            mapOf(
                "image" to attachment,
                "mimeType" to "image/jpeg",
            )
        )
    }
}

In order to stay up to date with other devices using our attachments app, we need to monitor the collection for any new inserts that may happen. For this, we will add an observer for our pics collection.

companion object {
    ...

    fun observePics(callback: (pics: List<Pic>, event: DittoLiveQueryEvent) -> Unit): DittoLiveQuery? {
       return DittoHandler.ditto?.store?.collection("pics")?.findAll()
           ?.observe{ docs, event ->
               val pics = docs.map { Pic(it) }
               callback(pics, event)
        }
    }
}

Lastly we need to be able to extract the data from the attachments using our DittoAttachmentToken. Here we fetch the attachment, get the data and write to a file

companion object {
    ...

    fun getDataForAttachmentToken(
       context: Context,
       token: DittoAttachmentToken,
       callback: (progress: Float?, error: Exception?, byteData: ByteArray?, file: File?) -> Unit
    ): DittoAttachmentFetcher? {
       return DittoHandler.ditto?.store?.collection("pics")?.fetchAttachment(token) { event ->
           when (event.type) {
               DittoAttachmentFetchEventType.Completed -> {
                   val data = event.asCompleted()?.attachment?.getData()
                   if (data != null) {
                       try {
                           val file = File(context.cacheDir, "${UUID.randomUUID()}.jpg")
                           file.writeBytes(data)
                           callback(1f, null, data, file)
                       } catch (e: Exception) {
                           callback(null, e, null, null)
                       }
                   } else {
                       callback(null, Exception("Attachment data is null"), null, null)
                   }
               }
               DittoAttachmentFetchEventType.Progress -> {
                   val downloadedBytes = event.asProgress()?.downloadedBytes
                   val totalBytes = event.asProgress()?.downloadedBytes
                   if (downloadedBytes != null && totalBytes != null) {
                       callback(
                           downloadedBytes.toFloat() / totalBytes.toFloat(),
                           null,
                           null,
                           null
                       )
                   }
               }
               else -> {
                   callback(null, Exception("Unknown error"), null, null)
               }
           }
       }
    }
}

Observing Data

Now we can incorporate our methods from the Pic class into our Activity!

To observe images with a DittoLiveQuery, add the following to the bottom of the onCreate method in your Activity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

    ...

    liveQuery = Pic.observePics { pics, event ->
       val adapter = (this.viewAdapter as PicsAdapter)
       when (event) {
           is DittoLiveQueryEvent.Initial -> {
               runOnUiThread {
                    adapter.setInitial(pics.toMutableList())
               }
           }
           is DittoLiveQueryEvent.Update -> {
               runOnUiThread {
                    //Update adapter with new data
                    adapter.set(pics)
               }
           }
       }
    }
}

Here we are creating an observer for the pics collection, setting our RecyclerView with the initial dataset as well as any updates that may occur.

Adding Images to a Collection

Add a simple button that performs an action to select an image from your device’s photo gallery:

val gallery = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI)
startActivityForResult(gallery, 1001)

Next we handle the result of the media picker intent and insert a picture using our Pic helper class. Add the code below to handle the file management.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == RESULT_OK && requestCode == 1001) {
        data?.data?.also { uri ->
            val path = getPathFromURI(uri)
            Pic.insertPicture(this, File(path))
        }
    }
}

fun getPathFromURI(contentUri: Uri?): String? {
    var cursor: Cursor? = null
    return try {
        val proj = arrayOf(MediaStore.Images.Media.DATA)
        cursor = contentUri?.let { contentResolver.query(it, proj, null, null, null) }
        val column_index: Int = cursor!!.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
        cursor.moveToFirst()
        cursor.getString(column_index)
    } finally {
        if (cursor != null) {
            cursor.close()
        }
    }
}

Displaying Images

Finally, in order to visually stay up to date with our pics collection, we can set our RecyclerView.Adapter item that contains an ImageView with an image from the collection.

First let’s create a custom class for our RecyclerView.Adapter. For your adapter item, create an ImageView that equals the bounds of it’s frame and a ProgressBar in the center. For this tutorial, we’ll be using Glide to load data into our ImageView.

In our adapter, create a list property of our Pic class that will keep hold of all the images in our pics collection. The DittoAttachmentFetcher must be kept alive for as long as you wish to observe updates about the associated.

class PicsAdapter(context: Context): RecyclerView.Adapter<PicsAdapter.PicViewHolder>() {
   private val pics = mutableListOf<Pic>()
   var attachmentFetcher: DittoAttachmentFetcher?
   ...
} 

Now we’ll add a method that will handle the fetching of a Pic image using the unique DittoAttachmentToken. Once the fetching of the data is complete, we can hide our progressView and set our pictureImageView image.

class PicsAdapter(context: Context): RecyclerView.Adapter<PicsAdapter.PicViewHolder>() {
    ...
    
    fun configureWithPic(itemView: View, pic: Pic) {
       val imageView = itemView.findViewById<AppCompatImageView>(R.id.imageView)
       val progressView = itemView.findViewById<ProgressBar>(R.id.progress)
       if (pic.imageAttachmentToken == null) {
           imageView.setImageDrawable(null)
           return
       }

       if (pic.mimetype == "image/jpeg") {
       attachmentFetcher = Pic.getDataForAttachmentToken(
               context,
               pic.imageAttachmentToken!!
           ) { progress, error, byteData, file ->
               GlobalScope.launch(Dispatchers.Main) {
                   if (progress == null || progress == 0f || progress == 1f) {
                       progressView.visibility = View.GONE
                   } else {
                       progressView.visibility = View.VISIBLE
                   }

                   if (progress != null) {
                       progressView.progress = progress.toInt()
                   } else {
                       progressView.progress = 0
                   }

                   if (byteData != null) {
                       Glide.with(context)
                           .asBitmap()
                           .load(byteData)
                           .into(imageView)
                   }
               }
           }
       }
    }
} 

Lastly, we can grab the Pic object at the adapter position and pass it to our configureWithPic method to set the item’s image

override fun onBindViewHolder(holder: PicViewHolder, position: Int) {
   val pic = pics[position]
   configureWithPic(holder.itemView, pic)
}

Build and Run!

🎉 You now have a fully functioning attachments app. Build and run it on a device. The simulator will not show any data sync because neither Bluetooth or the necessary network system is available to allow simulators to find each other or another device.

Android Attachments App Syncing
Top