TalkJS has support for file attachments with various file formats allowed. However when implementing a Native Android App using Kotlin or Java, the WebView by default does not provide the ability to upload files. This is not made clear in Android’s developer pages and documentation. This tutorial will show you how to add file upload to your app’s native WebView implementation.

If want to build a chat with TalkJS check out the getting started guide and sign up for free.

Prerequisites

For this tutorial we shall be using Kotlin. The concepts, types and method names being exactly the same as in Java with the differences only being in the syntax. Similarly, Android Studio and IntelliJ can convert Kotlin code to Java.

We will assume you already have an Android project setup in Android Studio or your favourite editor. Add a WebView to your Activity’s layout XML file. A simple implementation as the one below should be enough:

<WebView
   android:id="@+id/webview"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
 />

Make sure that you have the appropriate permissions in your AndroidManifest.xml file to allow us to upload files.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

In your Activity class, you’ll need to add 2 fields. This first, named: filePath, will hold the URI of the file the user wants to upload.

var filePath: ValueCallback<Array<Uri>>? = null

The second, html, contains the html document that will be loaded in the WebView. We are hardcoding it as a string just to simplify the code we’ll need to implement. Don’t forget to replace the string, YOUR_APP_ID, with your TalkJS app ID from the dashboard. Also, this tutorial assumes that you have enabled file transfer in the TalkJS dashboard for the specific roles assigned to the users below.

val html = """
   <!DOCTYPE html>
   <head>
     <script>
       (function(t,a,l,k,j,s){
       s=a.createElement('script');s.async=1;s.src="https://cdn.talkjs.com/talk.js";a.head.appendChild(s)
       ;k=t.Promise;t.Talk={v:3,ready:{then:function(f){if(k)return new k(function(r,e){l.push([f,r,e])});l
       .push([f])},catch:function(){return k&&new k()},c:l}};})(window,document,[]);
      </script>
   </head>
   <body>
     <div id="talkjs-container" style="width: 100%; height: 600px">
     </div>
   </body>
   <script>
     Talk.ready.then(function() {
       var me = new Talk.User({
         id: '432156789',
         name: 'Sebastian',
         email: 'Sebastian@example.com',
         photoUrl: 'https://demo.talkjs.com/marketplace_demo/img/sebastian.jpg',
         welcomeMessage: null,
         role: 'default',
       });
       var other = new Talk.User({
         id: '123456789',
         name: 'Alice',
         email: 'alice@example.com',
         photoUrl: 'https://demo.talkjs.com/marketplace_demo/img/alice.jpg',
         welcomeMessage: null,
         role: 'default',
       });

       window.talkSession = new Talk.Session({
         appId: 'YOUR_APP_ID',
         me: me,
       });

       var conversation = talkSession.getOrCreateConversation(Talk.oneOnOneId(me, other));
       console.log(conversation);

       conversation.setParticipant(me);
       conversation.setParticipant(other);

       conversation.setAttributes({ welcomeMessages: ["New conversation"], subject: "Conversation 1" });

       var chatbox = talkSession.createChatbox(conversation);
       chatbox.mount(document.getElementById('talkjs-container'));

       chatbox.select(conversation);
     });
   </script>

   </html>
""".trimIndent()

Extending the WebChromeClient Class

One of the most critical steps to enabling file uploads is by implementing a subclass of android.webkit.WebChromeClient. The subclass will need access to the Activity that has the filePath field we defined earlier. We shall do pass this Activity in the primary constructor as shown:

class MyWebChromeClient(private val myActivity: Activity) : WebChromeClient(){}

Inside this class, we need to override the method, onShowFileChooser. This method is called when the user clicks a HTML form with the input type as “file”. In our case, this is when a user clicks the file attachment button in the TalkJS UI.

override fun onShowFileChooser(
   webView: WebView?,
   filePathCallback: ValueCallback<Array<Uri>>?,
   fileChooserParams: FileChooserParams?
): Boolean {
   myActivity.filePath = filePathCallback

   val contentIntent = Intent(Intent.ACTION_GET_CONTENT)
   contentIntent.type = "*/*"
   contentIntent.addCategory(Intent.CATEGORY_OPENABLE)

   myActivity.getFile.launch(contentIntent)
   return true
}

We assign the filePath field from earlier to the filePathCallback parameter passed. Next, we create an Intent with the action, GET_CONTENT. This action specifies that we want the user to select a particular kind of file/data. Next, we indicate the MIME type of the data that we expect. "*/*" allows us to accept a wide range of data types. Adding the category OPENABLE to the intent indicates to the system that we only expect URIs that can be opened as a stream. This is useful due to the wide range of files that we accept. Finally, we use the intent to launch our Activity for the results. We shall define the getFile property in the next section.

Registering a callback for an Activity Result

In order to be able to process the result of the user selecting a file, we need to register a callback using the Activity Result APIs. Once the user selects a file, our callback will be called with the URI of the file being passed as a result. The important thing to do here is to use the filePath field we defined earlier to pass the URI back to the WebView and subsequently stream data to the TalkJS UI so that it can be uploaded. This is done by calling the onReceiveValue, passing the URI retrieved from the Intent data passed as a parameter.

Registering a callback returns an ActivityResultLauncher which we will store in a property called getFile in our Activity class. This property is what was used in the previous section to start the activity for a result.

val getFile = registerForActivityResult(
	ActivityResultContracts.StartActivityForResult()) {
   		if (it.resultCode == Activity.RESULT_CANCELED) {
       		filePath?.onReceiveValue(null)
   		} else if (it.resultCode == Activity.RESULT_OK && filePath != null) {
       		filePath!!.onReceiveValue(
           		WebChromeClient.FileChooserParams.parseResult(it.resultCode, it.data))
       		filePath = null
   		}
	}

Included are checks to determine if the user canceled the operation. In that case, we just need to pass null to onReceiveValue.

Wrapping Up

Finally, we need to setup our WebView in our Activity’s onCreate method. Here we assign the webview an instance of our WebChromeClient class that we defined earlier. We can then finally load our HTML string.

val webView: WebView = findViewById(R.id.webview)
webView.settings.javaScriptEnabled = true
webView.webChromeClient = MyWebChromeClient(this)

webView.loadDataWithBaseURL(
    "https://app.talkjs.com", html, "text/html", null, "https://app.talkjs.com")

With that, your users should be able to upload their memes, documents or videos to each other.

The code shown in this tutorial is available as an Android project on Github. You can open the project on Android Studio and run on your device or emulator.

You’ve successfully subscribed to TalkJS
Welcome back! You’ve successfully signed in.
Great! You’ve successfully signed up.
Your link has expired
Success! Check your email for magic link to sign-in.