How to prevent memory leak with In-App Update Library

Viewed 536

I want to implement the new In-App Update library in my app, but I've noticed that it trigger a memory leak in my activity when it's recreated/rotated.

Here's the only detail I have from LeakCanary:

LeakCanary trace

Obviously, I've nothing if I remove the code from the In-App Update lib especially the addOnSuccessListener :

appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
        && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
            updateInfo.value = appUpdateInfo
            updateAvailable.value = true
        }else{
            updateInfo.value = null
            updateAvailable.value = false
        }
    }

According to this post, I have first used some LiveData, but the problem was the same, so I used a full class to handle the callback, with LiveData :

My Service class :

class AppUpdateService {

    val updateAvailable: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateDownloaded: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateInfo: MutableLiveData<AppUpdateInfo> by lazy { MutableLiveData<AppUpdateInfo>() }

    fun checkForUpdate(appUpdateManager: AppUpdateManager){
        appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
                    && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
                updateInfo.value = appUpdateInfo
                updateAvailable.value = true
            }else{
                updateInfo.value = null
                updateAvailable.value = false
            }
        }
    }

    fun checkUpdateOnResume(appUpdateManager: AppUpdateManager){
        appUpdateManager.appUpdateInfo.addOnSuccessListener {
            updateDownloaded.value = (it.installStatus() == InstallStatus.DOWNLOADED)
        }
    }
}

My Activity simplified :

class MainActivity : BaseActivity(), InstallStateUpdatedListener {

    override fun contentViewID(): Int { return R.layout.activity_main }

    private val UPDATE_REQUEST_CODE = 8000

    private lateinit var appUpdateManager : AppUpdateManager

    private val appUpdateService = AppUpdateService()

    override fun onStateUpdate(state: InstallState?) {
        if(state?.installStatus() == InstallStatus.DOWNLOADED){ notifyUser() }
    }

    // Called in the onCreate()
    override fun setupView(){
        appUpdateManager = AppUpdateManagerFactory.create(this)
        appUpdateManager.registerListener(this)
        setupAppUpdateServiceObservers()
        // Check for Update
        appUpdateService.checkForUpdate(appUpdateManager)
    }

    private fun setupAppUpdateServiceObservers(){
        appUpdateService.updateAvailable.observe(this, Observer {
            if (it)
                requestUpdate(appUpdateService.updateInfo.value)
        })

        appUpdateService.updateDownloaded.observe(this, Observer {
            if (it)
                notifyUser()
        })
    }

    private fun requestUpdate(appUpdateInfo: AppUpdateInfo?){
        appUpdateManager.startUpdateFlowForResult(appUpdateInfo, AppUpdateType.FLEXIBLE, this, UPDATE_REQUEST_CODE)
    }

    private fun notifyUser(){
        showSnackbar(getString(R.string.updated_downloaded), getString(R.string.restart)) {
            appUpdateManager.completeUpdate()
            appUpdateManager.unregisterListener(this)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == UPDATE_REQUEST_CODE) {
            if (resultCode != RESULT_OK) {
                Timber.d("Update flow failed! Result code: $resultCode")
            }
        }
    }

    override fun onDestroy() {
        appUpdateManager.unregisterListener(this)
        super.onDestroy()
    }

    override fun onResume() {
        super.onResume()
        appUpdateService.checkUpdateOnResume(appUpdateManager)
    }

}

I don't really understand how to avoid the memory leak as the appUpdateManager has to be created with the context of the activity, and it looks to be the thing that causes the memory leak with the callback.

Does someone already implement it without having this issue?

3 Answers

Using weak reference to the context will probably solve your memory leak problem. Write this in your activity:

WeakReference<Context> contextWeakReference = new WeakReference<Context>(this);

Context context = contextWeakReference.get();
if (context != null) {
   // Register using context here
}

There are lots of good articles on WeakReference, Garbage Collection and Memory Leaks to read more on the subject.

Also, onDestroy() is not guaranteed to be called. When you start another Activity, onPause() and onStop() method called instead of onDestroy().

The onDestroy() calls when you hit back button or call finish() method. So, unregister Listener in onPause() or onStop(). If you unregister in onDestroy() method, it might cause a memory leak.

Another idea is that since AppUpdateService class in not a subclass of ViewModel, it is not lifecycle aware. I'm not sure, but, you might need to remove observers in onstop/onDestroy of the activity and add them in onResume. (observers has a strong reference to the LifecycleOwner, here the activiy) To do that you need to define observers to be able to remove them later. Something like:

MutableLiveData<Boolean> someData = new MutableLiveData<>;

and then in onResume:

someData = appUpdateService.updateAvailable;
someData.observe()

and in onStop:

someData.removeObservers()

It's just a guess, but, I hope it would help somehow.

Thanks to @Sina Farahzadi I searched and try a lot of things and figured that the problem was the appUpdateManager.appUdateInfo call with the Task object.

The way I found to solve the memory leak is to use the applicationContext instead of the context of the activity. I'm not sure it's the best solution, but it's the one I've found for now. I've exported all in my service class so here's my code :

AppUpdateService.kt :

class AppUpdateService : InstallStateUpdatedListener {

    val updateAvailable: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateDownloaded: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val notifyUser: MutableLiveData<Boolean> by lazy { MutableLiveData<Boolean>() }
    val updateInfo: MutableLiveData<AppUpdateInfo> by lazy { MutableLiveData<AppUpdateInfo>() }

    private var appUpdateManager : AppUpdateManager? = null
    private var appUpdateInfoTask: Task<AppUpdateInfo>? = null

    override fun onStateUpdate(state: InstallState?) {
        notifyUser.value =  (state?.installStatus() == InstallStatus.DOWNLOADED)
    }

    fun setupAppUpdateManager(context: Context){
        appUpdateManager = AppUpdateManagerFactory.create(context)
        appUpdateManager?.registerListener(this)
        checkForUpdate()
    }

    fun onStopCalled(){
        appUpdateManager?.unregisterListener(this)
        appUpdateInfoTask = null
        appUpdateManager = null
    }

    fun checkForUpdate(){
        appUpdateInfoTask = appUpdateManager?.appUpdateInfo
        appUpdateInfoTask?.addOnSuccessListener { appUpdateInfo ->
            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
                    && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)){
                updateInfo.value = appUpdateInfo
                updateAvailable.value = true
            }else{
                updateInfo.value = null
                updateAvailable.value = false
            }
        }
    }

    fun startUpdate(activity: Activity, code: Int){
        appUpdateManager?.startUpdateFlowForResult(updateInfo.value, AppUpdateType.FLEXIBLE, activity, code)
    }

    fun updateComplete(){
        appUpdateManager?.completeUpdate()
        appUpdateManager?.unregisterListener(this)
    }

    fun checkUpdateOnResume(){
        appUpdateManager?.appUpdateInfo?.addOnSuccessListener {
            updateDownloaded.value = (it.installStatus() == InstallStatus.DOWNLOADED)
        }
    }

}

MainActivity simplified :

class MainActivity : BaseActivity(){

    override fun contentViewID(): Int { return R.layout.activity_main }

    private val UPDATE_REQUEST_CODE = 8000

    private var appUpdateService: AppUpdateService? = AppUpdateService()

    /**
     * Setup the view of the activity (navigation and menus)
     */
    override fun setupView(){
        val contextWeakReference = WeakReference<Context>(applicationContext)
        contextWeakReference.get()?.let {weakContext ->
            appUpdateService?.setupAppUpdateManager(weakContext)
        }
    }

    private fun setupAppUpdateServiceObservers(){
        appUpdateService?.updateAvailable?.observe(this, Observer {
            if (it)
                requestUpdate()
        })

        appUpdateService?.updateDownloaded?.observe(this, Observer {
            if (it)
                notifyUser()
        })

        appUpdateService?.notifyUser?.observe(this, Observer {
            if (it)
                notifyUser()
        })
    }

    private fun removeAppUpdateServiceObservers(){
        appUpdateService?.updateAvailable?.removeObservers(this)
        appUpdateService?.updateDownloaded?.removeObservers(this)
        appUpdateService?.notifyUser?.removeObservers(this)
    }

    private fun requestUpdate(){
        appUpdateService?.startUpdate(this, UPDATE_REQUEST_CODE)
    }

    private fun notifyUser(){
        showSnackbar(getString(R.string.updated_downloaded), getString(R.string.restart)) {
            appUpdateService?.updateComplete()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == UPDATE_REQUEST_CODE) {
            if (resultCode != RESULT_OK) {
                Timber.d("Update flow failed! Result code: $resultCode")
            }
        }
    }

    override fun onStop() {
        appUpdateService?.onStopCalled()
        removeAppUpdateServiceObservers()
        appUpdateService = null
        super.onStop()
    }

    override fun onResume() {
        super.onResume()
        setupAppUpdateServiceObservers()
        appUpdateService?.checkUpdateOnResume()
    }

}

For now, I will keep it that way and continue to search for another way to do it. Let me know if someone has a better way to do it.

Use this helper class:

class GoogleUpdater(activity: FragmentActivity) : LifecycleObserver {
    private val appUpdateManager = AppUpdateManagerFactory.create(activity)
    private var installStateUpdatedListener: InstallStateUpdatedListener? = null
    private var wra = WeakReference(activity)
    private val activity get() = wra.get()

    init {
        activity.lifecycle.addObserver(this)
    }

    fun checkUpdate() {
        fun showCompleteUpdateDialog() {
            activity?.let { activity ->
                if (!activity.isFinishing)
                    AlertDialog.Builder(activity)
                            .setTitle(R.string.notification)
                            .setMessage(R.string.restart_to_complete_update)
                            .setIcon(ContextCompat.getDrawable(activity, R.drawable.ic_notification)
                                    ?.apply {
                                        mutate()
                                        alpha = 127
                                    })
                            .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> appUpdateManager.completeUpdate() }
                            .setNegativeButton(R.string.no, null)
                            .create()
                            .apply { setCanceledOnTouchOutside(false) }
                            .show()
            }
        }

        installStateUpdatedListener = object : InstallStateUpdatedListener {
            override fun onStateUpdate(state: InstallState) {
                if (state.installStatus() == InstallStatus.DOWNLOADED)
                    showCompleteUpdateDialog()
                else if (state.installStatus() == InstallStatus.INSTALLED)
                    appUpdateManager.unregisterListener(this)
            }
        }.also { appUpdateManager.registerListener(it) }

        appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
            val clientVersionStalenessDays = appUpdateInfo.clientVersionStalenessDays()
            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
                    && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
                    && clientVersionStalenessDays != null
                    && clientVersionStalenessDays >= DAYS_FOR_FLEXIBLE_UPDATE) {
                try {
                    activity?.let { activity ->
                        if (!activity.isFinishing)
                            appUpdateManager.startUpdateFlowForResult(
                                    appUpdateInfo,
                                    AppUpdateType.FLEXIBLE,
                                    activity,
                                    REQUEST_CODE_APP_UPDATE)
                    }
                } catch (e: SendIntentException) {
                    FirebaseCrashlytics.getInstance().recordException(e)
                }
            } else if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED)
                showCompleteUpdateDialog()
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    private fun onStop() {
        installStateUpdatedListener?.let { appUpdateManager.unregisterListener(it) }
    }

    companion object {
        const val REQUEST_CODE_APP_UPDATE = 11
        const val DAYS_FOR_FLEXIBLE_UPDATE = 1
    }
}

In Activity:

GoogleUpdater(this).apply { checkUpdate() }
Related