How To Fix Glide SSL Exception in Android Compose

A little Background First

I am working on an Android app that will control your TV. A simple remote. The remote in my house is always missing. I know there are plenty on the Play Store but I wanted to create a less bloated app than the ones I found on the app store.

One of the TV’s I own is a LG that uses Web OS. LGs website directs you to use ConnectSDK to develop an app that communicates with the TV.

This is the ConnectSDK website. https://connectsdk.com/en/latest/

The SDK is fairly straight forward. I will post more about my adventures through the SDK as I come across necessary information to share.

I was quickly able to search for devices, select devices, simple communication ( cursor movements), launch apps, get device info, etc.

Where I Ran Into Issues with the SSL Handshake

I created a simple remote controller UI that also lists all the apps installed on the device via ConnectSDK in JSON format. The list has icon images for the apps. This would look good in the remote UI for the user.

When I try to load the image via Glide, I get the SSL Exception. The exception is more specifically

Caused by: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

The ConnectSDK library does contain a Websocket with proper authentication. I was unable to find a method to use to utilize the same Websocket for Glide.

I reached out to stackoverflow as well as ConnectSDKs Github and was unsuccessful in getting a solution that worked for me.

My Questions Posted Below

The posts below show the different approaches I tried utilizing the library if anyone is interested.

https://stackoverflow.com/questions/79321036/android-connectsdk-display-the-app-icon-from-connected-tv-device-not-working-er

https://github.com/ConnectSDK/Connect-SDK-Android/issues/412

What I Did That Worked

I followed quite a few different posts and websites to make this work. It is a solution that is mixed between quite a few posts I viewed.

Helpful Links

https://stackoverflow.com/questions/41114569/glide-javax-net-ssl-sslhandshakeexception-java-security-cert-certpathvalidato

https://github.com/bumptech/glide/blob/master/integration/okhttp3/src/main/java/com/bumptech/glide/integration/okhttp3/OkHttpStreamFetcher.java

https://medium.com/@mustafayanik/how-to-fix-android-glide-ssl-exception-ab9b2c4dbada

Steps

I added the necessary libraries and annotation processor. I used KSP as annotation processor.

  1. Add to build.gradle (module)
plugins {    
    ....
    alias(libs.plugins.ksp)
}

....

dependencies {
....

    //for Glide
    ksp(libs.glide.ksp)
    implementation(libs.glide.compose)


}

2. Add to build.gradle (project)

plugins {    
    ....
    alias(libs.plugins.ksp) apply false
}

3. Add to libs.versions.toml

[versions]
ksp = "2.0.21-1.0.27"
composeVersion = "1.0.0-beta01"

....
[libraries]
glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "composeVersion" }
....

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}

4. I created a few files in a separate package to keep things in order as much as possible. Needed decent structure. I named my package glidestuff off the main package.

5. Inside CustomOkHtppClient

import okhttp3.OkHttpClient
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager


object CustomOkHttpClient {
    val customOkHttpClient: OkHttpClient
        get() {
            try {
                // Create a trust manager that does not validate certificate chains
                val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
                    @Throws(CertificateException::class)
                    override fun checkClientTrusted(
                        chain: Array<X509Certificate>,
                        authType: String
                    ) {
                    }

                    @Throws(CertificateException::class)
                    override fun checkServerTrusted(
                        chain: Array<X509Certificate>,
                        authType: String
                    ) {
                    }

                    override fun getAcceptedIssuers(): Array<X509Certificate> {
                        return arrayOf()
                    }
                }
                )

                // Install the all-trusting trust manager
                val sslContext = SSLContext.getInstance("SSL")
                sslContext.init(null, trustAllCerts, SecureRandom())

                // Create an ssl socket factory with our all-trusting manager
                val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory

                val builder: OkHttpClient.Builder = OkHttpClient.Builder()
                builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
                builder.hostnameVerifier(HostnameVerifier { hostname, session -> true })

                val okHttpClient: OkHttpClient = builder.build()
                return okHttpClient
            } catch (e: Exception) {
                throw RuntimeException(e)
            }
        }
}

6. Now inside CustomOkHttpGlideModule. This is what Glide will use to create a client connection. This can be customize for a variety of reasons, our reason is to bypass the SSL Handshake process.

import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import com.itgeek25.sampletvremote.glidestuff.CustomOkHttpClient.customOkHttpClient
import java.io.InputStream


@GlideModule
class CustomOkHttpGlideModule : AppGlideModule() {
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        val client = customOkHttpClient
        registry.replace(
            GlideUrl::class.java, InputStream::class.java,
            OkHttpUrlLoader.Factory(client)
        )
    }
}

7. Add to the OkHttpStreamFetcher

import android.util.Log
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.HttpException
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.data.DataFetcher.DataCallback
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.util.ContentLengthInputStream
import com.bumptech.glide.util.Preconditions
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import java.io.IOException
import java.io.InputStream
import kotlin.concurrent.Volatile


/** Fetches an [InputStream] using the okhttp library.  */
class OkHttpStreamFetcher(client: Call.Factory, private val url: GlideUrl) : DataFetcher<InputStream>,
    Callback {
    private val client: Call.Factory = client
    private var stream: InputStream? = null
    private var responseBody: ResponseBody? = null
    private var callback: DataCallback<in InputStream>? = null

    // call may be accessed on the main thread while the object is in use on other threads. All other
    // accesses to variables may occur on different threads, but only one at a time.
    @Volatile
    private var call: Call? = null

    override fun loadData(
        priority: Priority, callback: DataCallback<in InputStream>
    ) {
        val requestBuilder: Request.Builder = Request.Builder().url(url.toStringUrl())
        for ((key, value) in url.headers) {
            requestBuilder.addHeader(key, value)
        }
        val request: Request = requestBuilder.build()
        this.callback = callback

        call = client.newCall(request)
        call!!.enqueue(this)
    }

    override fun onFailure(call: Call, e: IOException) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "OkHttp failed to obtain result", e)
        }

        callback!!.onLoadFailed(e)
    }

    override fun onResponse(call: Call, response: Response) {
        responseBody = response.body
        if (response.isSuccessful) {
            val contentLength = Preconditions.checkNotNull(responseBody).contentLength()
            stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength)
            callback!!.onDataReady(stream)
        } else {
            callback!!.onLoadFailed(HttpException(response.message, response.code))
        }
    }

    override fun cleanup() {
        try {
            if (stream != null) {
                stream!!.close()
            }
        } catch (e: IOException) {
            // Ignored
        }
        if (responseBody != null) {
            responseBody!!.close()
        }
        callback = null
    }

    override fun cancel() {
        val local = call
        local?.cancel()
    }

    override fun getDataClass(): Class<InputStream> {
        return InputStream::class.java
    }

    override fun getDataSource(): DataSource {
        return DataSource.REMOTE
    }

    companion object {
        private const val TAG = "OkHttpFetcher"
    }
}

8. Add to OkHttpUrlLoader

import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoader.LoadData
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import okhttp3.Call
import okhttp3.OkHttpClient
import java.io.InputStream
import kotlin.concurrent.Volatile


/** A simple model loader for fetching media over http/https using OkHttp.  */
class OkHttpUrlLoader // Public API.
    (private val client: OkHttpClient) : ModelLoader<GlideUrl, InputStream> {
    override fun handles(url: GlideUrl): Boolean {
        return true
    }

    override fun buildLoadData(
        model: GlideUrl, width: Int, height: Int, options: Options
    ): LoadData<InputStream>? {
        return LoadData<InputStream>(model, OkHttpStreamFetcher(client as Call.Factory, model))
    }

    /** The default factory for [OkHttpUrlLoader]s.  */ // Public API.
    class Factory
    /** Constructor for a new Factory that runs requests using a static singleton client.  */ @JvmOverloads constructor(
        private val client: OkHttpClient = internalClient!! as OkHttpClient
    ) :
        ModelLoaderFactory<GlideUrl, InputStream> {
        /**
         * Constructor for a new Factory that runs requests using given client.
         *
         * @param client this is typically an instance of `OkHttpClient`.
         */

        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, InputStream> {
            return OkHttpUrlLoader(client)
        }

        override fun teardown() {
            // Do nothing, this instance doesn't own the client.
        }

        companion object {
            @Volatile
            private var internalClient: Factory? = null
                get() {
                    if (field == null) {
                        synchronized(Factory::class.java) {
                            if (field == null) {
                            //had an issue with below line, but after following breadcrumbs of where it would be needed, I realized my case it wasn't important
                                //field = OkHttpClient()
                            }
                        }
                    }
                    return field
                }
        }
    }
}

9. Now open your AndroidManifest.xml and add this inside your application tag

        <meta-data
            android:name="com.itgeek25.sampletvremote.data.glidestuff.CustomOkHttpGlideModule"
            android:value="AppGlideModule"/>

This tags and names are to point to your module you created in step #6 inside the CustomOkHttpGlideModule.class.

Now Test

I will just show you how I used Glide to load the icon image. There is quite a bit more about Glide that I need to learn but to get this to work, the basic element implementation is needed.

Your custom module created above is being point to by the AndroidManifest.

                    GlideImage(
                        model = appItem.icon,
                        contentDescription = appItem.title,
                        modifier = Modifier
                            .padding(10.dp)
                            .size(100.dp),
                    )

Hopefully this works for you. It took quite a bit of time for me to get this to work for my use case with a lot of trial and error.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *