All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.com.wisetrack.sdk.InstallReferrer.kt Maven / Gradle / Ivy

There is a newer version: 1.5.8-alpha
Show newest version
package com.wisetrack.sdk

import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.wisetrack.sdk.Constants.Companion.ONE_SECOND
import com.wisetrack.sdk.scheduler.SingleThreadCachedScheduler
import com.wisetrack.sdk.scheduler.ThreadExecutor
import com.wisetrack.sdk.scheduler.TimerOnce
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.atomic.AtomicBoolean

class InstallReferrer
    (
    private val context: Context,
    /**
     * Referrer callback.
     */
    private val referrerCallback: InstallReferrerReadListener
) : InvocationHandler {

    companion object {
        /**
         * Android install referrer library package name.
         */
        private const val PACKAGE_BASE_NAME = "com.android.installreferrer."

        /**
         * Play Store service is not connected now - potentially transient state.
         */
        private const val STATUS_SERVICE_DISCONNECTED = -1

        /**
         * Play Store service connection success.
         */
        private const val STATUS_OK = 0

        /**
         * Could not initiate connection to the install referrer service.
         */
        private const val STATUS_SERVICE_UNAVAILABLE = 1

        /**
         * Install Referrer API not supported by the installed Play Store app.
         */
        private const val STATUS_FEATURE_NOT_SUPPORTED = 2

        /**
         * General errors caused by incorrect usage.
         */
        private const val STATUS_DEVELOPER_ERROR = 3
    }

    /**
     * Retry time interval.
     */
    private val retryWaitTime: Int = (ONE_SECOND * 3).toInt()

    /**
     * Number of retries attempted to connect to service.
     */
    private var retries = 0

    /**
     * Boolean indicating whether service should be tried to read.
     * Either because it has not yet tried,
     * or it did and it was successful
     * or it did, was not successful, but it should not retry
     */
    private var shouldTryToRead: AtomicBoolean? = null


    private var logger: ILogger? = null

    /**
     * InstallReferrer class instance.
     */
    private var referrerClient: Any? = null

    /**
     * Timer which fires retry attempts.
     */
    private var retryTimer: TimerOnce? = null

    private var playInstallReferrer: Any? = null

    private var executor: ThreadExecutor? = null

    init {
        logger = WiseTrackFactory.getLogger()
        playInstallReferrer = createInstallReferrer(context, referrerCallback, logger!!)
        shouldTryToRead = AtomicBoolean(true)
        retries = 0
        retryTimer = TimerOnce({ startConnection() }, "InstallReferrer")
        executor = SingleThreadCachedScheduler("InstallReferrer")
    }

    private fun createInstallReferrer(
        context: Context,
        referrerCallback: InstallReferrerReadListener,
        logger: ILogger
    ): Any? {
        return Reflection.createInstance(
            "com.wisetrack.sdk.play.InstallReferrer", arrayOf(
                Context::class.java,
                InstallReferrerReadListener::class.java,
                ILogger::class.java
            ),
            context, referrerCallback, logger
        )
    }

    /**
     * Start connection with install referrer service.
     */
    fun startConnection() {
        if (playInstallReferrer != null) {
            try {
                Reflection.invokeInstanceMethod(playInstallReferrer!!, "startConnection", null)
                return
            } catch (e: Exception) {
                logger!!.error("Call to Play startConnection error: ${e.message!!}")
                logger!!.report("Call to Play startConnection error: ${e.message!!}")
            }
        }
        if (!WiseTrackFactory.tryInstallReferrer) {
            return
        }
        closeReferrerClient()
        if (!shouldTryToRead!!.get()) {
            logger!!.debug("Should not try to read Install referrer")
            return
        }
        referrerClient = createInstallReferrerClient(context)
        if (referrerClient == null) {
            return
        }
        val listenerClass = getInstallReferrerStateListenerClass() ?: return
        val listenerProxy: Any = createProxyInstallReferrerStateListener(listenerClass) ?: return
        startConnection(listenerClass, listenerProxy)
    }

    /**
     * Get object instance for given class (InstallReferrerStateListener in this case).
     *
     * @param installReferrerStateListenerClass Class object
     * @return Instance of Class type object.
     */
    private fun createProxyInstallReferrerStateListener(installReferrerStateListenerClass: Class<*>): Any? {
        var proxyInstance: Any? = null
        try {
            proxyInstance = Proxy.newProxyInstance(
                installReferrerStateListenerClass.classLoader,
                arrayOf(installReferrerStateListenerClass),
                this
            )
        } catch (ex: IllegalArgumentException) {
            logger!!.error("InstallReferrer proxy violating parameter restrictions")
            logger!!.report("InstallReferrer proxy violating parameter restrictions")
        } catch (ex: NullPointerException) {
            logger!!.error("Null argument passed to InstallReferrer proxy")
            logger!!.report("Null argument passed to InstallReferrer proxy")
        }
        return proxyInstance
    }

    /**
     * Initialise connection with install referrer service.
     *
     * @param listenerClass Callback listener class type
     * @param listenerProxy Callback listener object instance
     */
    private fun startConnection(listenerClass: Class<*>, listenerProxy: Any) {
        try {
            Reflection.invokeInstanceMethod(
                referrerClient!!,
                "startConnection",
                arrayOf(listenerClass),
                listenerProxy
            )
        } catch (ex: InvocationTargetException) {
            // Check for an underlying root cause in the stack trace
            if (Util.hasRootCause(ex)) {
                Util.getRootCause(ex)?.let {
                    logger!!.error("InstallReferrer encountered an InvocationTargetException $it")
                }
            }
        } catch (ex: Exception) {
            logger!!.error("startConnection error (${ex.message!!}) thrown by (${ex.javaClass.canonicalName})")
            logger!!.report("startConnection error (${ex.message!!}) thrown by (${ex.javaClass.canonicalName})")
        }
    }


    /**
     * Create InstallReferrerClient object instance.
     *
     * @param context App context
     * @return Instance of InstallReferrerClient. Defaults to null if failed to create one.
     */
    private fun createInstallReferrerClient(context: Context): Any? {
        try {
            val builder: Any? = Reflection.invokeStaticMethod(
                PACKAGE_BASE_NAME + "api.InstallReferrerClient",
                "newBuilder",
                arrayOf(Context::class.java), context
            )
            return builder?.let {
                Reflection.invokeInstanceMethod(it, "build", null)
            }
        } catch (ex: ClassNotFoundException) {
            logger!!.warn("InstallReferrer not integrated in project (${ex.message!!}) thrown by (${ex.javaClass.canonicalName})")
        } catch (ex: Exception) {
            logger!!.error("createInstallReferrerClient error (${ex.message!!}) from (${ex.javaClass.canonicalName})")
            logger!!.report("createInstallReferrerClient error (${ex.message!!}) from (${ex.javaClass.canonicalName})")
        }
        return null
    }

    /**
     * {@inheritDoc}
     */
    @Throws(Throwable::class)
    override fun invoke(proxy: Any?, method: Method?, args: Array?): Any? {
        executor!!.submit {
            try {
                invokeI(proxy, method, args)
            } catch (throwable: Throwable) {
                logger!!.error("invoke error (${throwable.message!!}) thrown by (${throwable.javaClass.canonicalName})")
                logger!!.report("invoke error (${throwable.message!!}) thrown by (${throwable.javaClass.canonicalName})")
            }
        }
        return null
    }

    @Throws(Throwable::class)
    private fun invokeI(proxy: Any?, method: Method?, args: Array?): Any? {
        var args: Array? = args
        if (method == null) {
            logger!!.error("InstallReferrer invoke method null")
            return null
        }
        val methodName = method.name
        if (methodName.isNullOrEmpty()) {
            logger!!.error("InstallReferrer invoke method name null")
            return null
        }
        // Prints the method being invoked
        logger!!.debug("InstallReferrer invoke method name: $methodName")
        if (args == null) {
            logger!!.warn("InstallReferrer invoke args null")
            args = arrayOfNulls(0)
        }
        for (arg in args) {
            logger!!.debug("InstallReferrer invoke arg: $arg!!")
        }

        // if the method name equals some method's name then call your method
        if (methodName == "onInstallReferrerSetupFinished") {
            if (args.size != 1) {
                logger!!.error("InstallReferrer invoke onInstallReferrerSetupFinished args length not 1: ${args.size}")
                return null
            }
            val arg = args[0]
            if (arg !is Int) {
                logger!!.error("InstallReferrer invoke onInstallReferrerSetupFinished arg not int")
                return null
            }
            val responseCode = arg as Int?
            if (responseCode == null) {
                logger!!.error("InstallReferrer invoke onInstallReferrerSetupFinished responseCode arg is null")
                return null
            }
            onInstallReferrerSetupFinishedIntI(responseCode)
        } else if (methodName == "onInstallReferrerServiceDisconnected") {
            logger!!.debug("Connection to install referrer service was lost. Retrying ...")
            retryI()
        }
        return null
    }

    /**
     * Get InstallReferrerStateListener class object.
     *
     * @return Class object for InstallReferrerStateListener class.
     */
    @SuppressLint("PrivateApi")
    private fun getInstallReferrerStateListenerClass(): Class<*>? {
        try {
            return Class.forName(PACKAGE_BASE_NAME + "api.InstallReferrerStateListener")
        } catch (ex: Exception) {
            logger!!.error("getInstallReferrerStateListenerClass error (${ex.message!!}) from (${ex.javaClass.canonicalName})")
            logger!!.report("getInstallReferrerStateListenerClass error (${ex.message!!}) from (${ex.javaClass.canonicalName})")
        }
        return null
    }

    /**
     * Check and process response from install referrer service.
     *
     * @param responseCode Response code from install referrer service
     */
    private fun onInstallReferrerSetupFinishedIntI(responseCode: Int) {
        var retryAtEnd = false
        when (responseCode) {
            STATUS_OK ->
                // Connection established
                try {
                    // Extract referrer
                    val referrerDetails = getInstallReferrer()


                    val clickTime = getReferrerClickTimestampSeconds(referrerDetails)
                    val installBegin = getInstallBeginTimestampSeconds(referrerDetails)


                    val installVersion = getStringInstallVersion(referrerDetails)
                    val clickTimeServer = getReferrerClickTimestampServerSeconds(referrerDetails)
                    val installBeginServer = getInstallBeginTimestampServerSeconds(referrerDetails)
                    val googlePlayInstant = getBooleanGooglePlayInstantParam(referrerDetails)
                    logger!!.debug(
                        "installVersion: ${installVersion}, clickTimeServer: ${clickTimeServer}, " +
                                "installBeginServer: ${installBeginServer}, googlePlayInstant: $googlePlayInstant"
                    )
                    logger!!.debug("Install Referrer read successfully. Closing connection")


                    val installReferrer =
                        getStringInstallReferrer(referrerDetails, googlePlayInstant, installVersion)
                            ?: ("\'utm_source=" + getStoreName() + "&utm_medium=organic\'")



                    val installReferrerDetails = ReferrerDetails(
                        installReferrer,
                        clickTime, installBegin, clickTimeServer, installBeginServer,
                        installVersion, googlePlayInstant
                    )


                    // Stuff successfully read, try to send it.
                    referrerCallback.onInstallReferrerRead(
                        installReferrerDetails,
                        Constants.REFERRER_API_GOOGLE
                    )

                } catch (e: java.lang.Exception) {
                    logger!!.warn(
                        "Couldn't get install referrer from client (%s). Retrying...",
                        e.message!!
                    )
                    retryAtEnd = true
                }

            STATUS_FEATURE_NOT_SUPPORTED ->
                // API not available on the current Play Store app
                logger!!.debug("Install Referrer API not supported by the installed Play Store app. Closing connection")

            STATUS_SERVICE_UNAVAILABLE -> {
                // Connection could not be established
                logger!!.debug("Could not initiate connection to the Install Referrer service. Retrying...")
                retryAtEnd = true
            }

            STATUS_SERVICE_DISCONNECTED -> {
                // Play Store service is not connected now - potentially transient state
                logger!!.debug("Play Store service is not connected now. Retrying...")
                retryAtEnd = true
            }

            STATUS_DEVELOPER_ERROR -> {
                logger!!.debug("Install Referrer API general errors caused by incorrect usage. Retrying...")
                retryAtEnd = true
            }

            else -> logger!!.debug("Unexpected response code of install referrer response: $responseCode. Closing connection")
        }
        if (retryAtEnd) {
            retryI()
        } else {
            shouldTryToRead!!.set(false)
            closeReferrerClient()
        }
    }

    /**
     * Get ReferrerDetails object (response).
     *
     * @return ReferrerDetails object
     */
    private fun getInstallReferrer(): Any? {
        if (referrerClient == null) {
            return null
        }
        try {
            return Reflection.invokeInstanceMethod(
                referrerClient!!, "getInstallReferrer", null
            )
        } catch (e: java.lang.Exception) {
            logger!!.error("getInstallReferrer error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
            logger!!.report("getInstallReferrer error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
        }
        return null
    }

    /**
     * Get install referrer string value.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Install referrer string value.
     */
    private fun getStringInstallReferrer(
        referrerDetails: Any?,
        googlePlayInstance: Boolean?,
        installVersion: String?
    ): String? {
        if (referrerDetails == null) {
            return null
        }
        try {



            // This code retrieves the application's metadata using PackageManager and
            // ApplicationInfo. It specifically fetches the value associated with the key
            // "wisetrack_storeName" from the metadata bundle, which presumably contains
            // information related to the source store. It then uses reflection to invoke
            // the "getInstallReferrer" method from the provided "referrerDetails" object,
            // which likely handles obtaining the installation referrer information.
            // The obtained store name and referrer are stored in the variables "storeName"
            // and "referrer" respectively for further processing.
            val ai: ApplicationInfo = context!!.getPackageManager()
                .getApplicationInfo(context!!.getPackageName(), PackageManager.GET_META_DATA)
            val bundle = ai.metaData
            val storeName = bundle.getString("wisetrack_storeName")


            val referrer = Reflection.invokeInstanceMethod(
                referrerDetails, "getInstallReferrer", null
            ) as String?


            // This conditional block is responsible for processing the referrer URL based on
            // the presence or absence of a "storeName" parameter. If "storeName" is not null,
            // it replaces any occurrence of "google-play" in the referrer with the provided
            // store name. If "storeName" is null, it checks whether "googlePlayInstance" is
            // not null. If it's not null, it checks whether "googlePlayInstance" is false
            // and "installVersion" is null. If both conditions are true, it replaces
            // "google-play" with "other". Otherwise, it returns the referrer unchanged.

            if (storeName != null)
                return referrer!!.replace("google-play", storeName)
            else {
                googlePlayInstance?.let {
                    if (!googlePlayInstance && installVersion == null)
                        return referrer!!.replace("google-play", "other")
                    else
                        return referrer
                } ?: return referrer

            }


        } catch (e: java.lang.Exception) {
            logger!!.error("getStringInstallReferrer error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
            logger!!.report("getStringInstallReferrer error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
        }
        return null
    }

    /**
     * Get redirect URL click timestamp.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Redirect URL click timestamp.
     */
    private fun getReferrerClickTimestampSeconds(referrerDetails: Any?): Long {
        if (referrerDetails == null) {
            return -1
        }
        try {
            return Reflection.invokeInstanceMethod(
                referrerDetails, "getReferrerClickTimestampSeconds", null
            ) as Long
        } catch (e: java.lang.Exception) {
            logger!!.error("getReferrerClickTimestampSeconds error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
            logger!!.report("getReferrerClickTimestampSeconds error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
        }
        return -1
    }

    /**
     * Get Play Store app INSTALL button click timestamp.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Play Store app INSTALL button click timestamp.
     */
    private fun getInstallBeginTimestampSeconds(referrerDetails: Any?): Long {
        if (referrerDetails == null) {
            return -1
        }
        try {
            return Reflection.invokeInstanceMethod(
                referrerDetails, "getInstallBeginTimestampSeconds", null
            ) as Long
        } catch (e: java.lang.Exception) {
            logger!!.error("getInstallBeginTimestampSeconds error (%${e.message!!}) thrown by (${e.javaClass.canonicalName})")
            logger!!.report("getInstallBeginTimestampSeconds error (%${e.message!!}) thrown by (${e.javaClass.canonicalName})")
        }
        return -1
    }

    /**
     * Get install version string value.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Install version string value.
     */
    private fun getStringInstallVersion(referrerDetails: Any?): String? {
        if (referrerDetails == null) {
            return null
        }
        try {
            return Reflection.invokeInstanceMethod(
                referrerDetails, "getInstallVersion", null
            ) as String?
        } catch (e: java.lang.Exception) {
            // not logging the error as this is expected to happen below v2.0
        }
        return null
    }

    /**
     * Get redirect URL click timestamp server.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Redirect URL click timestamp server.
     */
    private fun getReferrerClickTimestampServerSeconds(referrerDetails: Any?): Long {
        if (referrerDetails == null) {
            return -1
        }
        try {
            return Reflection.invokeInstanceMethod(
                referrerDetails, "getReferrerClickTimestampServerSeconds", null
            ) as Long
        } catch (e: java.lang.Exception) {
            // not logging the error as this is expected to happen below v2.0
        }
        return -1
    }

    /**
     * Get Play Store app INSTALL button click timestamp server.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Play Store app INSTALL button click timestamp server.
     */
    private fun getInstallBeginTimestampServerSeconds(referrerDetails: Any?): Long {
        if (referrerDetails == null) {
            return -1
        }
        try {
            return Reflection.invokeInstanceMethod(
                referrerDetails, "getInstallBeginTimestampServerSeconds", null
            ) as Long
        } catch (e: java.lang.Exception) {
            // not logging the error as this is expected to happen below v2.0
        }
        return -1
    }

    /**
     * Get google play instant boolean value.
     *
     * @param referrerDetails ReferrerDetails object
     * @return Google play instant boolean value.
     */
    private fun getBooleanGooglePlayInstantParam(referrerDetails: Any?): Boolean? {
        if (referrerDetails == null) {
            return null
        }

        try {
            return Reflection.invokeInstanceMethod(
                referrerDetails, "getGooglePlayInstantParam", null
            ) as Boolean
        } catch (e: java.lang.Exception) {
            // not logging the error as this is expected to happen below v2.0
        }
        return null
    }

    /**
     * Retry connection to install referrer service.
     */
    private fun retryI() {
        if (!shouldTryToRead!!.get()) {
            logger!!.debug("Should not try to read Install referrer")
            closeReferrerClient()
            return
        }
        // Check increase retry counter
        if (retries + 1 > Constants.MAX_INSTALL_REFERRER_RETRIES) {
            logger!!.debug("Limit number of retry of ${Constants.MAX_INSTALL_REFERRER_RETRIES} for install referrer surpassed")
            return
        }
        val firingIn = retryTimer!!.getFireIn()
        if (firingIn > 0) {
            logger!!.debug("Already waiting to retry to read install referrer in $firingIn milliseconds")
            return
        }
        retries++
        logger!!.debug("Retry number $retries to connect to install referrer API")
        retryTimer!!.startIn(retryWaitTime.toLong())
    }

    /**
     * Terminate connection to install referrer service.
     */
    private fun closeReferrerClient() {
        if (referrerClient == null) {
            return
        }
        try {
            Reflection.invokeInstanceMethod(referrerClient!!, "endConnection", null)
            logger!!.debug("Install Referrer API connection closed")
        } catch (e: java.lang.Exception) {
            logger!!.error("closeReferrerClient error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
            logger!!.report("closeReferrerClient error (${e.message!!}) thrown by (${e.javaClass.canonicalName})")
        }
        referrerClient = null
    }


    /**
     * Get store name value string.
     */
    private fun getStoreName(): String? {

        // get store name of meta-data that user add to yourself project's manifest file
        val ai: ApplicationInfo = context!!.getPackageManager()
            .getApplicationInfo(context!!.getPackageName(), PackageManager.GET_META_DATA)
        val bundle = ai.metaData
        val storeName = bundle.getString("wisetrack_storeName")

        if (storeName != null)
            return storeName
        else
            return "other"

    }


    /**
     * Generate ReferrerDetails object by manually
     */
    fun getManuallyInstallReferrerDetail(): ReferrerDetails? {
        try {
            // Extract referrer
            val referrerDetails = getInstallReferrer()


            val clickTime = getReferrerClickTimestampSeconds(referrerDetails)
            val installBegin = getInstallBeginTimestampSeconds(referrerDetails)

            val installVersion = getStringInstallVersion(referrerDetails)
            val clickTimeServer = getReferrerClickTimestampServerSeconds(referrerDetails)
            val installBeginServer = getInstallBeginTimestampServerSeconds(referrerDetails)
            val googlePlayInstant = getBooleanGooglePlayInstantParam(referrerDetails)


            val installReferrer =
                getStringInstallReferrer(referrerDetails, googlePlayInstant, installVersion)
                    ?: "utm_source=" + getStoreName() + "&utm_medium=organic"

            val installReferrerDetails = ReferrerDetails(
                installReferrer,
                clickTime, 0, 0, 0,
                installVersion, googlePlayInstant
            )

            return installReferrerDetails
        } catch (e: java.lang.Exception) {
            logger!!.warn(
                "Couldn't get install referrer from client (%s) by manually. Retrying...",
                e.message!!
            )
            return null
        }

        return null
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy