Search code examples
androidkotlinreturnlogic

How do I wait for a function to be done executing before returning a value?


I have an issue with this code where the return packageSize statement is triggered before the onGetStatsCompleted function and it returns 0 instead of the right value. Is there a way I can force onGetStatsCompleted to finish before returning packageSize? I know it's a logic issue because if I remove the comment at //Thread.sleep it works fine.

How do I fix this without using Thread.sleep or any other kind of time out in the application? ORIGINAL CODE:

/**
Get the size of the app for API < 26
*/
@Throws(InterruptedException::class)
fun getPackageSize(): Long {

    val pm = context.packageManager
    try {
        val getPackageSizeInfo = pm.javaClass.getMethod(
                "getPackageSizeInfo", String::class.java, IPackageStatsObserver::class.java)
        getPackageSizeInfo.invoke(pm, context.packageName,
                object : CachePackState() {//Call inner class
                })
    } catch (e: Exception) {
        e.printStackTrace()
    }
    //Thread.sleep(1000)
    return packageSize
}

/**
  Inner class which will get the data size for the application
 */
open inner class CachePackState : IPackageStatsObserver.Stub() {

    override fun onGetStatsCompleted(pStats: PackageStats, succeeded: Boolean) {
        //here the pStats has all the details of the package
        dataSize = pStats.dataSize
        cacheSize = pStats.cacheSize
        apkSize = pStats.codeSize
        packageSize = cacheSize + apkSize

    }
}

EDIT CODE:

This is the StorageInformation class

import android.annotation.SuppressLint
import android.app.usage.StorageStatsManager
import android.content.Context
import android.content.pm.IPackageStatsObserver
import android.content.pm.PackageManager
import android.content.pm.PackageStats


/**
This class will perform data operation
 */
internal class StorageInformation(internal var context: Context) {

    private var packageSize: Long = 0
    private var dataSize: Long = 0
    private var cacheSize: Long = 0
    private var apkSize: Long = 0

    /**
    Get the size of the app
     */
    @Throws(InterruptedException::class)
    suspend fun getPackageSize(): Long {

        val pm = context.packageManager

        @SuppressLint("WrongConstant")
        val storageStatsManager: StorageStatsManager
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
            try {
                val ai = context.packageManager.getApplicationInfo(context.packageName, 0)
                val storageStats = storageStatsManager.queryStatsForUid(ai.storageUuid, pm.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).uid)
                cacheSize = storageStats.cacheBytes
                apkSize = storageStats.appBytes
                packageSize = cacheSize + apkSize
            } catch (e: Exception) {
                e.printStackTrace()
            }

        } else {
            try {
                val getPackageSizeInfo = pm.javaClass.getMethod(
                        "getPackageSizeInfo", String::class.java, IPackageStatsObserver::class.java)
                getPackageSizeInfo.invoke(pm, context.packageName,
                        object : CachePackState() {//Call inner class
                        })
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        return packageSize
    }

    /**
    Inner class which will get the data size for the application
     */
    open inner class CachePackState : IPackageStatsObserver.Stub() {

        override fun onGetStatsCompleted(pStats: PackageStats, succeeded: Boolean) {
            //here the pStats has all the details of the package
            dataSize = pStats.dataSize
            cacheSize = pStats.cacheSize
            apkSize = pStats.codeSize
            packageSize = cacheSize + apkSize

        }
    }
}

Calling StorageInformation from an interface

    var appSize=""
    fun getPackageSize(callback: (Long) -> Unit) {
        launch(Dispatchers.IO) {
            val size = StorageInformation(getApplicationContext()).getPackageSize()
            callback(size)
        }
    }
    fun handlePackageSize(size: Long) {
        launch(Dispatchers.Main) {
            appSize = formatFileSize(getApplicationContext(), size)
        }
    }
    getPackageSize(::handlePackageSize)

I also tried the solution from r2rek and get the same result

    try {
        GlobalScope.launch(Dispatchers.Main){
            var getPackageSizeInfo = withContext(coroutineContext) {
                pm.javaClass.getMethod(
                        "getPackageSizeInfo", String::class.java, IPackageStatsObserver::class.java)
            }
            getPackageSizeInfo.invoke(pm, context.packageName,
                    object : CachePackState() {//Call inner class
                    })
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
return packageSize

Feel free to ask any questions, any help is appreciated.


Solution

  • The easiest way is to use kotlin coroutines and their suspend functions.

    Start by adding them to your project

    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
    

    Then all you need to do is just add suspend modifier to your method signature, so it looks like this.

    suspend fun getPackageSize(): Long {...}
    

    and then you can obtain it like this

    fun collectAndShow(){
        launch(Dispatchers.IO){
            val size = getPackageSize()
            withContext(Dispatchers.Main){
                textView.text = "App size is: $size"
            }
        }
    }
    

    I would recommend that you make your Activity, Service, ViewModel implement CoroutineScope which can help you prevent memory leaks. If you don't want to do that use GlobalScope.launch but you should definitely go with the 1st approach.

    So it looks like this

    class MainActivity : AppCompatActivity(), CoroutineScope {
        override val coroutineContext: CoroutineContext
            get() = Job()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            launch(Dispatchers.IO) {
                val size= getPackageSize()
                withContext(Dispatchers.Main){
                    findViewById<TextView>(R.id.textView).text="App size is: $size"
                }
            }
    
        }
    
        suspend fun getPackageSize(): Long {
           //do your stuff
        }
    }
    

    Another reason to use kotlin coroutines is that some jetpack libraries are gonna or already are supporting suspend functions.

    EDIT: If you cannot expose suspend functions then you can handle it using callbacks

    fun getPackageSize(callback: (Long) -> Unit) {
        launch(Dispatchers.IO) {
            ...
            val size = StorageInformation(getApplicationContext()).getPackageSize()
            callback(size)
        }
    }
    

    and then in your other class call it like this

        //wherever you want to get size
        ....
        getPackageSize(::handlePackageSize)
        ....
    
    fun handlePackageSize(size: Long) {
        //do whatever you want with size
        launch(Dispatchers.Main) {
            findViewById<TextView>(R.id.textView).text = "APP SIZE= $size"
        }
    }
    

    Again it's non-blocking, the way it should be!