0

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.

phil652
  • 1,484
  • 1
  • 23
  • 48

4 Answers4

5

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!

svkaka
  • 3,942
  • 2
  • 31
  • 55
  • This seems like a possible solution. I get unresolved reference Dispatchers and cannot find the import. I also have `kotlin.coroutines=enable` in gradle.properties. Do you know why? – phil652 May 14 '19 at 13:41
  • @phil652 have you added coroutines to your project? https://github.com/Kotlin/kotlinx.coroutines#gradle both core and android part – svkaka May 14 '19 at 14:25
  • you should be able to access `kotlinx.coroutines.Dispatchers` if you added dependency correctly – svkaka May 14 '19 at 14:29
  • @phil652 yep, I just checked the combination of kotlinVersion = 1.2.61 and coroutinesVersion = 1.2.1 works fine. Can you show what kotlin dependency are u using? – svkaka May 15 '19 at 07:56
  • I added the 2 implementation for the coroutines in my build.gradle. Dispatchers is resolved but i get `Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: public fun CoroutineScope.launch(context: CoroutineContext = ..., start: CoroutineStart = ..., block: suspend CoroutineScope.() → Unit): Job defined in kotlinx.coroutine` and `'withContext(CoroutineContext, suspend CoroutineScope.() -> T): T' is only available since Kotlin 1.3 and cannot be used in Kotlin 1.2` – phil652 May 15 '19 at 12:29
  • @phil652 is updating to Kotlin 1.3 something that you cannot do? because what I proposed follows the guide provided by kotlin team. E.g here https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055 – svkaka May 15 '19 at 12:34
  • I'm currently working on that on another branch but it's not ready yet. So no not at the moment, but I will be updating to Kotlin 1.3 in the near future – phil652 May 15 '19 at 12:40
  • @phil652 I found out what was your problem if you don't want to upgrade kotlin version you need to use a very old coroutine version meaning `0.30.2` for both core and android. Then it will work as described – svkaka May 15 '19 at 13:01
  • I already had `api "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"` in my build.gadle as a dependency, is it possible that this is causing issues when adding coroutine core and android – phil652 May 15 '19 at 19:08
  • I managed to implement coroutines in my project, I updated my question with the current code. I still get 0. What am I doing wrong ? – phil652 May 16 '19 at 00:12
  • In your non-suspend function you are returning 0, that's the reason. You assign size:Long = 0; then you move to a different thread (inside is executed on a different thread) and then you are returning 0 because it's not waiting for the result and you don't want it to wait. You can either mark this function as suspend and then whenever you need result just call it inside e.g `launch`. In you case you can just move `val appSize = formatFileSize(getApplicationContext(),getPackageSize())` to inside of `GlobalScope.launch(Dispatchers.IO){` and do you action there. – svkaka May 16 '19 at 08:46
  • Thanks for your help, this is being used in an email template so I can't really update the UI inside the `GlobalScope.launch(Dispatchers.IO){` Is there any way of doing this by just modifying a variable inside the CoroutineScope . I can add more of my code if needed – phil652 May 16 '19 at 12:01
  • I updates my question with something else I tried, I still get no value – phil652 May 16 '19 at 12:46
  • @phil652 I updated answer showing how to handle it differently – svkaka May 16 '19 at 12:53
  • With this I get `Module with the Main dispatcher is missing`. This might be because I'm using an older version – phil652 May 16 '19 at 14:09
  • @phil652 do you have coroutines android dependency? – svkaka May 16 '19 at 14:57
1

I highly recommend you to do that work on the background thread using RxJava, coroutines or an AsyncTask. But you could use a ContdownLatch to do a quick fix.

//Ugly global variable
val countdownLatch = CountdownLatch(1) //-------CHANGE HERE--------

/**
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()
    }
    countDownLatch.await(1_000, TimeUnit.MILLISECONDS) //-------CHANGE HERE--------
    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
        countDownLatch.countDown() //-------CHANGE HERE--------
    }
}

For more information on how it works check this great answer here: https://stackoverflow.com/a/17827339/7926889

Luciano Ferruzzi
  • 1,042
  • 9
  • 20
0

Using Thread.sleep(..) is not only not recommended, it might also lock the UI, and not produce the result that you want(if the getPackageSizeInfo method runs longer than 1 second). I'd strongly suggest getting info on background thread, using either AsyncTask or Coroutines, as @Luciano-Ferruzzi suggested. Since you're already using kotlin, I'd go for native solution and use coroutines, which might look something like this:

GlobalScope.launch(Dispatchers.Main){

  val getPackageSizeInfo = withContext(Dispacthers.IO) {
pm.javaClass.getMethod(
                "getPackageSizeInfo", String::class.java, IPackageStatsObserver::class.java)
        getPackageSizeInfo.invoke(pm, context.packageName,
                object : CachePackState() {//Call inner class
                })
     }
}

As you can see, this basically makes no changes in your code, except for explicitly stating the threads you'd be using for specific parts of the code.

*Sorry for any code errors, I didn't really compile it.

r2rek
  • 2,083
  • 13
  • 16
0

This is fairly old school but how about:

@Volatile
private  var packageSize: Long = -1

and then in fun getPackageSize() replace Thread.sleep with:

while(packageSize < 0) {
    Thread.sleep(100)
}
David Soroko
  • 8,521
  • 2
  • 39
  • 51
  • So far this is the only working solution. I just don't know if it's a good solution, Thread.sleep blocks a thread and from what I've learn best practice is to stay away from them. I could be wrong, I'm still very new to Kotlin. Also this could become an infinite loop if for some reason packageSize is always < 0 – phil652 May 16 '19 at 03:09
  • 1) `Thread.sleep` is a basic core Java functionality so I am curious about your "best practices" comment. You are experiencing a race condition so some sort of thread coordination is required. 2) If `packageSize` may always remain negative, that is absolutely a concern which you will need to tackle. You could use the `CountDownLatch` approach which is essentially the same thing with a bit more machinery and allows you wait conditionally with `await(long timeout, TimeUnit unit)` – David Soroko May 16 '19 at 07:11
  • @DavidSoroko are you seriously recommending him to block the UI for 100ms?! – svkaka May 16 '19 at 08:57
  • I don't recommend any particular value for sleep, the code will be correct regardles of the duration, or with no sleep at all if spinning the CPU is not a concern. – David Soroko May 16 '19 at 09:06