'Disable firebase logging for google ML Kit library in android

Could anyone help me to tell how can I disable the firebase logging in Google ML Kit library for android. For every 15 mins it will POST some information to https://firebaselogging.googleapis.com/v0cc/log/batch?format=json_proto3

I tried using the recommendation from google https://firebase.google.com/docs/perf-mon/disable-sdk?platform=android#kotlin+ktx and I am also not sure whether it is a right way.

Suggestions are welcome.



Solution 1:[1]

Google's guide from the question didn't work for me, so I have looked for alternatives.

The library is obfuscated, so it is hard to be sure, but it appears that the logging is hardcoded in. However, there is a very hacky way to disable it through some fragile reflection:

import android.util.Log
import com.google.mlkit.common.sdkinternal.LazyInstanceMap
import java.lang.reflect.Field

/**
 * This class tries to disable MLKit's phoning home/logging.
 * This is extremely hacky and will probably break in the next update (obfuscated class names will probably need renaming).
 *
 * This class exploits the fact, that there are multiple options classes which control this
 * (look for "MLKitLoggingOptions" in toString implementation) and for some reason MLKit uses them as keys
 * in LazyInstanceMaps which exist as static (usually) variables (which are themselves lazy).
 *
 * This makes sure that the LazyInstanceMaps exist, then it hijacks their internal HashMap implementation
 * and replaces it with a custom map, that creates instances of whatever with logging disabled.
 *
 * The way to detect which holder classes need renaming, look at the stack trace, for example:
 * ```
    java.lang.NoClassDefFoundError: Failed resolution of: Lcom/google/android/datatransport/cct/CCTDestination;
    at com.google.android.gms.internal.mlkit_vision_barcode.zznu.<init>(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:1)
    at com.google.android.gms.internal.mlkit_vision_barcode.zznf.<init>(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:3)
    at com.google.android.gms.internal.mlkit_vision_barcode.zznw.create(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:4)
    at com.google.mlkit.common.sdkinternal.LazyInstanceMap.get(com.google.mlkit:common@@18.0.0:3)
    at com.google.android.gms.internal.mlkit_vision_barcode.zznx.zza(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:2)
    at com.google.android.gms.internal.mlkit_vision_barcode.zznx.zzb(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:3)
    at com.google.mlkit.vision.barcode.internal.zzf.create(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:3)
    at com.google.mlkit.common.sdkinternal.LazyInstanceMap.get(com.google.mlkit:common@@18.0.0:3)
    at com.google.mlkit.vision.barcode.internal.zze.zzb(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:2)
    at com.google.mlkit.vision.barcode.BarcodeScanning.getClient(com.google.android.gms:play-services-mlkit-barcode-scanning@@18.0.0:3)
 * ```
 * here are two LazyInstanceMap lookups, of which only the second one (through trial and error or with debugger)
 * uses MLKitLoggingOptions keys. From here we can find that the holder class is com.google.android.gms.internal.mlkit_vision_barcode.zznx .
 */
object MLKitTrickery {

    private class mlkit_vision_barcodeLoggingOptions(base: com.google.android.gms.internal.mlkit_vision_barcode.zzne) : com.google.android.gms.internal.mlkit_vision_barcode.zzne() {
        private val libraryName: String = base.zzb()
        private val firelogEventType: Int = base.zza()
        override fun zza(): Int = firelogEventType
        override fun zzb(): String = libraryName
        override fun zzc(): Boolean = false //enableFirelog

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as mlkit_vision_barcodeLoggingOptions
            if (libraryName != other.libraryName) return false
            if (firelogEventType != other.firelogEventType) return false
            return true
        }

        override fun hashCode(): Int {
            var result = libraryName.hashCode()
            result = 31 * result + firelogEventType
            return result
        }
    }

    private class mlkit_vision_commonLoggingOptions(base: com.google.android.gms.internal.mlkit_vision_common.zzjn) : com.google.android.gms.internal.mlkit_vision_common.zzjn() {
        private val libraryName: String = base.zzb()
        private val firelogEventType: Int = base.zza()
        override fun zza(): Int = firelogEventType
        override fun zzb(): String = libraryName
        override fun zzc(): Boolean = false //enableFirelog

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as mlkit_vision_commonLoggingOptions
            if (libraryName != other.libraryName) return false
            if (firelogEventType != other.firelogEventType) return false
            return true
        }

        override fun hashCode(): Int {
            var result = libraryName.hashCode()
            result = 31 * result + firelogEventType
            return result
        }
    }

    private fun isMLKitLoggingOptions(obj: Any): Boolean {
        return obj is com.google.android.gms.internal.mlkit_vision_barcode.zzne
                || obj is com.google.android.gms.internal.mlkit_vision_common.zzjn
    }

    private fun convertMLKitLoggingOptions(obj: Any): Any? {
        if (obj is com.google.android.gms.internal.mlkit_vision_barcode.zzne) {
            return mlkit_vision_barcodeLoggingOptions(obj)
        }
        if (obj is com.google.android.gms.internal.mlkit_vision_common.zzjn) {
            return mlkit_vision_commonLoggingOptions(obj)
        }
        return null
    }

    @Suppress("UNCHECKED_CAST")
    private fun patchLazyMap(lazyMapHolder:Any?, lazyMapHolderClass: Class<*>) {
        val holderField = lazyMapHolderClass.declaredFields.find { LazyInstanceMap::class.java.isAssignableFrom(it.type) }!!
        var currentLazyInstanceMap = holderField.get(lazyMapHolder)
        if (currentLazyInstanceMap == null) {
            var lastError: Throwable? = null
            for (constructor in holderField.type.declaredConstructors) {
                try {
                    constructor.isAccessible = true
                    val params = arrayOfNulls<Any?>(constructor.parameterCount)
                    currentLazyInstanceMap = constructor.newInstance(*params)
                    holderField.set(lazyMapHolder, currentLazyInstanceMap)
                } catch (e:Throwable) {
                    lastError = e
                }
            }
            if (currentLazyInstanceMap == null) {
                throw java.lang.Exception("Failed to initialize LazyInstanceMap "+holderField.type, lastError)
            }
        }

        var mapHolderClass: Class<*> = currentLazyInstanceMap.javaClass
        val createMethod = mapHolderClass.getDeclaredMethod("create", Object::class.java)

        val mapField: Field
        while (true) {
            val mapFieldCandidate = mapHolderClass.declaredFields.firstOrNull { Map::class.java.isAssignableFrom(it.type) }
            if (mapFieldCandidate != null) {
                mapField = mapFieldCandidate
                break
            }
            mapHolderClass = mapHolderClass.superclass ?: error("It appears that ${currentLazyInstanceMap.javaClass} does not have a backing map field")
        }

        val oldMap = mapField.get(currentLazyInstanceMap) as MutableMap<Any, Any?>
        val customMap = object : MutableMap<Any, Any?> by oldMap {

            override fun containsKey(key: Any): Boolean {
                if (oldMap.containsKey(key)) {
                    return true
                }
                if (isMLKitLoggingOptions(key)) {
                    return true
                }
                return false
            }

            override fun get(key: Any): Any? {
                val existing = oldMap.get(key)
                if (existing != null) {
                    return existing
                }

                val convertedKey = convertMLKitLoggingOptions(key)
                if (convertedKey != null) {
                    val created = createMethod.invoke(currentLazyInstanceMap, convertedKey)
                    oldMap.put(key, created)
                    return created
                }

                return null
            }
        }
        mapField.isAccessible = true
        mapField.set(currentLazyInstanceMap, customMap)
    }

    private var initialized = false

    /**
     * Call this to attempt to disable MLKit logging.
     */
    fun init() {
        try {
            patchLazyMap(null, com.google.android.gms.internal.mlkit_vision_barcode.zznx::class.java)
            patchLazyMap(null, com.google.android.gms.internal.mlkit_vision_common.zzkc::class.java)
            initialized = true
        } catch (e: Throwable) {
            Log.e("MLKitTrickery", "Failed to disable MLKit phoning home")
        }
    }
}

When you also shim out GMS TelemetryLogging with:

@file:Suppress("unused", "UNUSED_PARAMETER")

package com.google.android.gms.common.internal

import android.app.Activity
import android.content.Context
import android.os.Parcel
import com.google.android.gms.tasks.OnFailureListener
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.gms.tasks.Task
import java.util.concurrent.Executor

class TelemetryLoggingOptions {
    class Builder {
        fun setApi(api: String?): Builder = this
        fun build(): TelemetryLoggingOptions = TelemetryLoggingOptions()
    }

    companion object {
        @JvmStatic
        fun builder(): Builder = Builder()
    }
}

private object DummyLogTask : Task<Void?>() {
    override fun addOnFailureListener(p0: OnFailureListener): Task<Void?> {
        // Implemented, because failing tells MLKit to back-off for 30 minutes, which is a win for performance
        p0.onFailure(exception)
        return this
    }
    override fun addOnFailureListener(p0: Activity, p1: OnFailureListener): Task<Void?> = addOnFailureListener(p1)
    override fun addOnFailureListener(p0: Executor, p1: OnFailureListener): Task<Void?> = addOnFailureListener(p1)

    override fun addOnSuccessListener(p0: OnSuccessListener<in Void?>): Task<Void?> = this
    override fun addOnSuccessListener(p0: Activity, p1: OnSuccessListener<in Void?>): Task<Void?> = addOnSuccessListener(p1)
    override fun addOnSuccessListener(p0: Executor, p1: OnSuccessListener<in Void?>): Task<Void?> = addOnSuccessListener(p1)

    override fun getException(): Exception? = exception
    override fun getResult(): Void? = null
    override fun <X : Throwable?> getResult(p0: Class<X>): Void? = null

    override fun isCanceled(): Boolean = false
    override fun isComplete(): Boolean = true
    override fun isSuccessful(): Boolean = false

    private val exception = Exception("Success was never an option")
}

object TelemetryLogging {
    @JvmStatic
    fun getClient(context: Context): TelemetryLoggingClient {
        return object : TelemetryLoggingClient {
            override fun log(data: TelemetryData): Task<Void?> {
                return DummyLogTask
            }
        }
    }

    @JvmStatic
    fun getClient(context: Context, options: TelemetryLoggingOptions): TelemetryLoggingClient {
        return getClient(context)
    }
}

interface TelemetryLoggingClient {
    fun log(data: TelemetryData): Task<Void?>
}

class TelemetryData(var1: Int, var2:List<MethodInvocation>?) {
    fun writeToParcel(var1: Parcel, var2: Int) {}
}

class MethodInvocation {

    constructor(methodKey:Int, resultStatusCode:Int, connectionResultStatusCode:Int,
                startTimeMillis:Long, endTimeMillis:Long,
                callingModuleId: String?, callingEntryPoint: String?, serviceId:Int)

    constructor(methodKey:Int, resultStatusCode:Int, connectionResultStatusCode:Int,
                startTimeMillis:Long, endTimeMillis:Long,
                callingModuleId: String?, callingEntryPoint: String?,
                serviceId:Int, var11:Int)

    fun writeToParcel(var1: Parcel, var2: Int) {}
}

it is possible to trim many transitive dependencies and save apk size:

implementation("com.google.mlkit:barcode-scanning:17.0.2") {
   exclude("com.google.android.gms", "play-services-base")
   exclude("com.google.android.datatransport", "transport-api")
   exclude("com.google.android.datatransport", "transport-backend-cct")
   exclude("com.google.android.datatransport", "transport-runtime")
   exclude("com.google.firebase", "firebase-encoders-json")
   exclude("com.google.firebase", "firebase-encoders")
}

However, as noted above, this is very fragile and will probably somehow break after MLKit update. It would be nice if this was not needed.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Darkyen