Showcase Recipes

Image Messages

Messages these days are no longer limited to text. Sending image messages in a chat is a common activity. This Recipe shows you how to quickly build support for sending/receiving image messages in your app.

Platform Android
Level Intermediate
Github View on GitHub
Time Required ~45 minutes
Where it matters - Any basic chat app

Prerequisite: Get your basic project setup done by following the steps mentioned over here. Once you’re done implementing a basic chat in your app, follow the steps below.

1 Step 1

The very first thing that you need to do is to add an option to pick an image from the gallery or camera using Android’s implicit intents.

Start by adding a button to your chat window and add the following code the button’s click listener:

val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
startActivityForResult(
   Intent.createChooser(intent, "Select Picture"),
   100
)

Now, override the onActivityResult() method to receive the selected image returned by the picker.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)

   if (resultCode == Activity.RESULT_OK) {
       if (requestCode == 100) {
           val imageUri = data?.data
           val previewImageIntent = Intent(this, PhotoActivity::class.java)
           previewImageIntent.putExtra("previewImage", imageUri)
           previewImageIntent.putExtra("channelId", channelId)

           startActivity(previewImageIntent)
       }
   }
}

2 Step 2

You can see that we opened up an activity called PhotoActivity. However, we haven’t created that activity, yet. The PhotoActivity will allow the user to preview the selected image, type in a message and send the image message to the chat.

Let’s create the activity. Add the layout structure in the activity’s XML file:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:tools="http://schemas.android.com/tools"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       tools:context=".MainActivity">

   <ImageView
           android:id="@+id/previewImage"
           android:layout_width="300dp"
           android:layout_height="300dp"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintBottom_toTopOf="@id/divider"/>

   <View
           android:id="@+id/divider"
           android:layout_width="0dp"
           android:layout_height="1dp"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           android:background="#e6e6e6"
           android:layout_marginBottom="10dp"
           app:layout_constraintBottom_toTopOf="@id/messageEditText"
   />

   <android.support.v7.widget.AppCompatEditText
           android:id="@+id/messageEditText"
           android:layout_width="0dp"
           android:layout_height="60dp"
           android:background="@drawable/back_input"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintEnd_toStartOf="@id/sendButton"
           android:layout_marginTop="10dp"
           android:layout_marginBottom="10dp"
           android:layout_marginStart="20dp"
           android:layout_marginEnd="20dp"
           android:padding="10dp"
           app:layout_constraintBottom_toBottomOf="parent"/>

   <android.support.v7.widget.AppCompatImageView
           android:id="@+id/sendButton"
           android:layout_width="32dp"
           android:layout_height="32dp"
           android:src="@drawable/ic_send_black_24dp"
           app:layout_constraintTop_toTopOf="@id/divider"
           app:layout_constraintEnd_toEndOf="parent"
           android:layout_marginEnd="20dp"
           app:layout_constraintBottom_toBottomOf="parent"/>

</android.support.constraint.ConstraintLayout>

Once that’s done, add the following code to your PhotoActivity.kt file:

class PhotoActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_photo)

       val imageUri = intent.getParcelableExtra<Uri>("previewImage")
       val channelId = intent.getStringExtra("channelId")

       Glide.with(this)
           .load(imageUri)
           .into(previewImage)

       val mitter = (application as App).mitter
       val messaging = mitter.Messaging()

       val id = DocumentsContract.getDocumentId(imageUri)
       val inputStream = contentResolver.openInputStream(imageUri)
       val file = File("${cacheDir.absolutePath}/$id.jpg")
       writeFile(inputStream, file)
}

   fun writeFile(inStream: InputStream, file: File) {
       var out: OutputStream? = null
       try {
           out = FileOutputStream(file)
           val buf = ByteArray(1024)
           var len: Int
           len = inStream.read(buf)
           while (len > 0) {
               out.write(buf, 0, len)
               len = inStream.read(buf)
           }
       } catch (e: Exception) {
           e.printStackTrace()
       } finally {
           try {
               if (out != null) {
                   out.close()
               }
               inStream.close()
           } catch (e: IOException) {
               e.printStackTrace()
           }

       }
   }
}

Here, we are retrieving the passed image URI and channel ID and making a local copy of the image file in our app space to get an absolute File path to be later passed on the SDK for uploading.

Note: You’ll need to add Glide to your project for the above code to work. You’re free to use any other image loading library you want.

3 Step 3

The next step is to wrap up the selected image and the type text into an image message and send it to Mitter.io.

The SDK handles all the complexity behind this for you and you can quickly send an image message by adding this lean piece of code to your PhotoActivity’s onCreate() method:

sendButton?.setOnClickListener {
   messaging.sendImageMessage(
       channelId = channelId,
       caption = messageEditText.text.toString(),
       file = file,
       onValueUpdatedCallback = object : Mitter.OnValueUpdatedCallback {
           override fun onError(apiError: ApiError) {
               Toast.makeText(this@PhotoActivity, "Please try again!", Toast.LENGTH_SHORT).show()
           }

           override fun onSuccess() {
               finish()
           }
       }
   )

   messageEditText.text.clear()
}

This will upload the image from the absolute path that you specified, send an image message to the channel and close the preview activity to return to the main chat screen.

4 Step 4

Now that we have successfully sent out an image message in the channel. It’s to display the message for both sides of the chat list.

Get started by adding the following two layouts for rendering the image message bubble.

For self messages:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

   <LinearLayout
           android:layout_width="250dp"
           android:layout_height="wrap_content"
           android:background="@drawable/back_blue_rounded_subtle"
           android:orientation="vertical"
           app:layout_constraintBottom_toBottomOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           android:layout_margin="5dp"
           app:layout_constraintTop_toTopOf="parent">

       <android.support.v7.widget.AppCompatImageView
               android:id="@+id/selfMessageImage"
               android:layout_width="match_parent"
               android:layout_height="250dp"
               tools:src="@drawable/ic_insert_photo_black_24dp"
               android:layout_margin="5dp"
       />

       <android.support.v7.widget.AppCompatTextView
               android:id="@+id/selfMessageCaption"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_margin="5dp"
               android:paddingBottom="10dp"
               android:paddingEnd="20dp"
               android:paddingStart="20dp"
               android:paddingTop="10dp"
               android:textColor="@android:color/white"
               android:textSize="16sp"
               tools:text="Hi, there!"/>

   </LinearLayout>

</android.support.constraint.ConstraintLayout>

For messages by others:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

   <LinearLayout
           android:layout_width="250dp"
           android:layout_height="wrap_content"
           android:background="@drawable/back_grey_rounded_subtle"
           android:orientation="vertical"
           android:layout_margin="5dp"
           app:layout_constraintBottom_toBottomOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent">

       <android.support.v7.widget.AppCompatImageView
               android:id="@+id/otherMessageImage"
               android:layout_width="match_parent"
               android:layout_height="250dp"
               tools:src="@drawable/ic_insert_photo_black_24dp"
               android:layout_margin="5dp"
       />

       <android.support.v7.widget.AppCompatTextView
               android:id="@+id/otherMessageCaption"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_margin="5dp"
               android:paddingBottom="10dp"
               android:paddingEnd="20dp"
               android:paddingStart="20dp"
               android:paddingTop="10dp"
               android:textColor="@android:color/black"
               android:textSize="16sp"
               tools:text="Hi, there!"/>

   </LinearLayout>

</android.support.constraint.ConstraintLayout>

5 Step 5

The last step is to add logic to your ChatRecyclerViewAdapter to display the image messages based on the currently logged in user and the message sender ID.

Modify your existing ChatRecyclerViewAdapter to look like this:

class ChatRecyclerViewAdapter(
   private val messageList: List<Message>,
   private val currentUserId: String
) : RecyclerView.Adapter<ChatRecyclerViewAdapter.ViewHolder>() {
   private val objectMapper = ObjectMapper()

   private val MESSAGE_SELF_VIEW = 0
   private val MESSAGE_OTHER_VIEW = 1
   private val MESSAGE_SELF_IMAGE_VIEW = 2
   private val MESSAGE_OTHER_IMAGE_VIEW = 3

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
       val layoutId = when (viewType) {
           MESSAGE_SELF_VIEW -> R.layout.item_message_self
           MESSAGE_SELF_IMAGE_VIEW -> R.layout.item_image_message_self
           MESSAGE_OTHER_IMAGE_VIEW -> R.layout.item_image_message_other
           else -> R.layout.item_message_other
       }
       val itemView = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
       return ViewHolder(itemView)
   }

   override fun getItemCount(): Int = messageList.size

   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
       holder.bindMessage(messageList[position])
   }

   override fun getItemViewType(position: Int) = when (messageList[position].payloadType) {
       StandardPayloadTypeNames.TextMessage ->
           if (messageList[position].senderId.domainId() == currentUserId)
               MESSAGE_SELF_VIEW else MESSAGE_OTHER_VIEW
       StandardPayloadTypeNames.ImageMessage ->
           if (messageList[position].senderId.domainId() == currentUserId)
               MESSAGE_SELF_IMAGE_VIEW else MESSAGE_OTHER_IMAGE_VIEW
       else -> MESSAGE_OTHER_VIEW
   }

   inner class ViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView) {
       fun bindMessage(message: Message) {
           with(message) {
               if (senderId.domainId() == currentUserId) {
                   if (payloadType == StandardPayloadTypeNames.TextMessage) {
                       itemView?.selfMessageText?.text = textPayload
                   } else {
                       val remoteImage = objectMapper.treeToValue(messageData[0].data, RemoteImage::class.java)

                       Glide.with(itemView.context)
                           .load(getRemoteImagePath(remoteImage))
                           .centerCrop()
                           .into(itemView.selfMessageImage)
                       itemView?.selfMessageCaption?.text = textPayload
                   }
               } else {
                   if (payloadType == StandardPayloadTypeNames.TextMessage) {
                       itemView?.otherMessageText?.text = textPayload
                   } else {
                       val remoteImage = objectMapper.treeToValue(messageData[0].data, RemoteImage::class.java)

                       Glide.with(itemView.context)
                           .load(getRemoteImagePath(remoteImage))
                           .centerCrop()
                           .into(itemView.otherMessageImage)
                       itemView?.otherMessageCaption?.text = textPayload
                   }
               }
           }
       }

       private fun getRemoteImagePath(remoteImage: RemoteImage): String =
           "https://api.mitter.io${remoteImage.link.replace("\$repr", remoteImage.repr[0])}"
   }
}

Also, add the following model class for using the attached image on an image message:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
data class RemoteImage(
   @JsonProperty("link") val link: String,
   @JsonProperty("repr") val repr: List<String>
)

Here, we are rendering an incoming message based on a combination of the two parameters: sender ID and message payload type.

If the payload type is TextMessage then render is normally, else deserialize the message data into a RemoteImage and use the image link and the first representation type to display the remote image.

Note: If you’re using Mitter.io deployed on your private server then you need to replace “https://api.mitter.io” with your server IP or common name.

Final result

Try running the app now and send image messages from one user to another using two devices or emulator instances. You should see the following result:
View recipe on Github