how to avoid running android Job in parallel? - android

So I have an Android Job which handle network request.
This job can started in many ways, so it can run easily parralel, which is bad for me.
I would like to achive that, the job don't started twice, or if it started, than wait before the try catch block, until the first finishes.
So how can I to reach that, just only one object be/run at the same time.
I tried add TAG and setUpdateCurrent false, but it didn't do anything, so when I started twice the job, it rung parallel. After that I tried mutex lock, and unlock. But it did the same thing.
With mutex, I should have to create an atomic mutex, and call lock by uniq tag or uuid?
Ok, so I figured it out what is the problem with my mutex, the job create a new mutex object every time, so it never be the same, and it never will wait.
My Job:
class SendCertificatesJob #Inject constructor(
private val sendSync: SendSync,
private val sharedPreferences: SharedPreferences,
private val userLogger: UserLogger,
private val healthCheckApi: HealthCheckApi
) : Job(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
private val countDownLatch = CountDownLatch(1)
private val mutex = Mutex()
override fun onRunJob(params: Params): Result {
var jobResult = Result.SUCCESS
if (!CommonUtils.isApiEnabled(context))
return jobResult
val notificationHelper = NotificationHelper(context)
var nb: NotificationCompat.Builder? = null
if (!params.isPeriodic) {
nb = notificationHelper.defaultNotificationBuilder.apply {
setContentTitle(context.resources.getString(R.string.sending_certificates))
.setTicker(context.resources.getString(R.string.sending_certificates))
.setOngoing(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.mipmap.ic_launcher
)
)
}
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobStart()
}
val failureCount = params.failureCount
if (failureCount >= 3) {
nb?.setOngoing(false)
?.setContentTitle(context.resources.getString(R.string.sending_certificates_failed))
?.setTicker(context.resources.getString(R.string.sending_certificates_failed))
?.setProgress(100, 100, false)
?.setSmallIcon(android.R.drawable.stat_sys_warning)
notificationHelper.notify(NOTIFICATION_ID, nb)
return Result.FAILURE
}
GlobalScope.launch(Dispatchers.IO) {
mutex.lock()
userLogger.writeLogToFile("SendCertificatesJob.onRunJob(), date:" + Calendar.getInstance().time)
try {
//Test
var doIt = true
var count =0
while (doIt){
Timber.d("SendSyncWorker: $count")
count++
delay(10000)
if(count == 12)
doIt = false
}
healthCheckApi.checkHealth(ApiModule.API_KEY).await()
try {
sendSync.syncRecordedClients()
} catch (e: Exception) {
e.printStackTrace()
}
val result = sendSync().forEachParallel2()
result.firstOrNull { it.second != null }?.let { throw Exception(it.second) }
val sb = StringBuilder()
if (nb != null) {
nb.setOngoing(false)
.setContentTitle(context.resources.getString(R.string.sending_certificates_succeeded))
.setTicker(context.resources.getString(R.string.sending_certificates_succeeded))
.setProgress(100, 100, false)
.setStyle(NotificationCompat.BigTextStyle().bigText(sb.toString()))
.setSmallIcon(android.R.drawable.stat_notify_sync_noanim)
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobEnd()
}
sharedPreferences.edit().putLong(KEY_LATEST_CERTIFICATES_SEND_DATE, Date().time)
.apply()
} catch (e: Exception) {
Timber.tag(TAG).e(e)
if (nb != null) {
nb.setOngoing(false)
.setContentTitle(context.resources.getString(R.string.sending_certificates_failed))
.setTicker(context.resources.getString(R.string.sending_certificates_failed))
.setProgress(100, 100, false)
.setSmallIcon(android.R.drawable.stat_sys_warning)
notificationHelper.notify(NOTIFICATION_ID, nb)
jobCallback?.jobEnd()
}
jobResult = Result.RESCHEDULE
} finally {
countDownLatch.countDown()
mutex.unlock()
}
}
countDownLatch.await()
return jobResult
}
Job schedule:
fun scheduleNowAsync(_jobCallback: JobCallback? = null) {
jobCallback = _jobCallback
JobRequest.Builder(TAG_NOW)
.setExecutionWindow(1, 1)
.setBackoffCriteria(30000, JobRequest.BackoffPolicy.LINEAR)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.scheduleAsync()
}
fun schedulePeriodicAsync() {
jobCallback = null
JobRequest.Builder(TAG)
.setPeriodic(900000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.scheduleAsync()
}

I found the solution for my problem.
So because I use dagger, I provided a singleton Mutex object, and injected into the job. When the job starts call mutex.lock(), and beacuse there is only 1 object from the mutex, even if another job starts, the second job will waite until the firsjob is done.

Related

Kotlin running code in multiple Scopes works, but not when ran in the same one

I am trying to poplulate values from a datastore. I only want to recover them from the datastore once, which is why I am canceling the job after 1 second. This prevents it constantly updating.
This does not work. (1)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does not work. (2)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
}
This does work! (3)
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
val job = Job()
val job2 = Job()
val job3 = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
val scope2 = CoroutineScope(job2 + Dispatchers.IO)
val scope3 = CoroutineScope(job3 + Dispatchers.IO)
scope.launch {
tempDataStore.getDieOne.collect {
die1.value = it!!.toInt()
}
}
scope2.launch {
tempDataStore.getDisplayText.collect {
displayText.value = it!!
}
}
scope3.launch {
tempDataStore.getDieTwo.collect {
die2.value = it!!.toInt()
}
}
delay(1000L)
job.cancel()
job2.cancel()
job3.cancel()
}
Here is the TempDataStore class (4)
class TempDataStore(private val context: Context) {
companion object{
private val Context.dataStore by preferencesDataStore(name = "TempDataStore")
val DISPLAY_TEXT_KEY = stringPreferencesKey("display_text")
val DIE_ONE = stringPreferencesKey("die_one")
val DIE_TWO = stringPreferencesKey("die_two")
}
val getDisplayText: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DISPLAY_TEXT_KEY] ?: "Roll to start!"
}
suspend fun saveDisplayText(text: String) {
context.dataStore.edit { preferences ->
preferences[DISPLAY_TEXT_KEY] = text
}
}
val getDieOne: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_ONE] ?: "1"
}
suspend fun saveDieOne(dieOne: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_ONE] = dieOne.toString()
}
}
val getDieTwo: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[DIE_TWO] ?: "2"
}
suspend fun saveDieTwo(dieTwo: Int) {
context.dataStore.edit { preferences ->
preferences[DIE_TWO] = dieTwo.toString()
}
}
suspend fun resetDataStore() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}
It is being called from a composable screen.
LaunchedEffect(true) {
sharedViewModel.setRoles()
sharedViewModel.saveChanges()
sharedViewModel.setupDataStore(context)
}
I was expecting (1) to work. It should run all of them at the same time and return the results accordingly. Instead of populating all of the values, it only populates the first one called. (3), works but I want to understand why it works and not (1) and (2).
Calling collect on a Flow suspends the coroutine until the Flow completes, but a Flow from DataStore never completes because it monitors for changes forever. So your first call to collect prevents the other code in your coroutine from ever being reached.
I'm not exactly why your second and third attempts aren't working, but they are extremely hacky anyway, delaying and cancelling as a means to avoid collecting forever.
Before continuing, I think you should remove the nullability from your Flow types:
val getDieOne: Flow<String?>
should be
val getDieOne: Flow<String>
since you are mapping to a non-nullable value anyway.
I don't know exactly what you're attempting, but I guess it is some initial setup in which you don't need to repeatedly update from the changing values in the Flows, so you only need the first value of each flow. You can use the first value to get that. Since these are pulling from the same data store, there's not really any reason to try to do it in parallel. So your function is pretty simple:
suspend fun setupDataStore(context: Context) {
with(TempDataStore(context)) {
die1.value = getDieOne.first().toInt()
displayText.value = getDisplayText.first()
die2.value = getDieTwo.first().toInt()
}
}
If you want just the first value, why not using the .first() method of Flow? And then you shouldn't need those new scopes!
Try out something like this:
suspend fun setupDataStore(context: Context) {
tempDataStore = TempDataStore(context)
die1.value = tempDataStore.getDieOne.first().toInt()
displayText.value = tempDataStore.getDisplayText.first()
die2.value = tempDataStore.getDieTwo.first().toInt()
}
EDIT:
Thanks Tenfour04 for the comment! You're right. I've fixed my code.

How to cancel a coroutine if already running?

I'm looking for filter a list on user input (SearchView)
fun onQuery(query: String) {
viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}
If the user tap quickly on the keyboard the function will be called many times and sometimes before the previous call is finished. So I'm looking to stop the coroutine if a new call is done and the coroutine is already running. How can I achieved this please ?
What I tried :
fun onQuery(query: String) {
val job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
if (job.isActive) job.cancel()
job.start()
}
If there is supposed to be only one instance of that job running, then I would try taking a reference to it outside of that code block and storing it as a private variable inside the class in which the method onQuery operates:
private var job: Job? = null
...
fun onQuery(query: String) {
job?.run { if (isActive) cancel() }
job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}

Kotlin: CoroutineScope is collecting data even after flow is cancelled

I am trying to launch an Coroutine inside my PagingSource in order to watch how long my paging source is already trying to get my data. The only problem I have here is, that my Coroutine is still somehow collecting some data, even after I stopped my shopPagingWatcher Flow. Because of this, it throws IOException("No Intenet Exception) even when it should not.
I am launching a Coroutine because watching the state should not block the main flow of my paging source
PagingSource
class ShopRemoteMediator #Inject constructor(
private val db: FirebaseFirestore,
private val shopPagingWatcher: ShopPagingWatcher,
) : PagingSource<QuerySnapshot, Product>() {
#InternalCoroutinesApi
override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Product> {
return try {
// Launch Async Coroutine, Observe State, throw IO Exception when not loaded within 5 seconds
shopPagingWatcher.start()
CoroutineScope(Dispatchers.IO).launch {
shopPagingWatcher.observeMaxTimeReached().collect { maxTimeReached ->
if (maxTimeReached) {
Timber.d("Mediator failed")
throw IOException("No Internet Exception")
}
}
}
val currentPage = params.key ?: db.collection(FIREBASE_PRODUCTS)
.limit(SHOP_LIST_LIMIT)
.get()
.await()
val lastDocumentSnapShot = currentPage.documents[currentPage.size() - 1]
val nextPage = db.collection(FIREBASE_PRODUCTS)
.limit(SHOP_LIST_LIMIT)
.startAfter(lastDocumentSnapShot)
.get()
.await()
// When PagingSource is here, it successfully loaded currentPage and nextPage, therefore stop Watcher
Timber.d("Mediator Sucessfull")
shopPagingWatcher.stop()
LoadResult.Page(
data = currentPage.toObjects(),
prevKey = null,
nextKey = nextPage
)
} catch (e: Exception) {
// IOException should be caught here, but it is not! The app crashed instead!
Timber.d("Mediator Exception ist $e")
LoadResult.Error(e)
}
}
}
ShopPagingWatcher
#Singleton
class ShopPagingWatcher #Inject constructor() : Workwatcher()
Abstract WorkWatcher
abstract class Workwatcher {
private companion object {
private val dispatcher = Dispatchers.IO
private var timeStamp by Delegates.notNull<Long>()
private var running = false
private var manuallyStopped = false
private var finished = false
private const val maxTime: Long = 5000000000L
}
// Push the current timestamp, set running to true
// I don't know if it is necessary to use "synchronized"
#InternalCoroutinesApi
fun start() = synchronized(dispatcher) {
timeStamp = System.nanoTime()
running = true
manuallyStopped = false
finished = false
}
// Manually stop the WorkerHelper
// I don't know if it is necessary to use "synchronized"
#InternalCoroutinesApi
fun stop() = synchronized(dispatcher) {
running = false
manuallyStopped = true
finished = true
Timber.d("Mediator stopped")
}
// Function that observes the time
fun observeMaxTimeReached(): Flow<Boolean> = flow {
// Check if maxTime is not passed with → (System.nanoTime() - timeStamp) <= maxTime
while (running && !finished && !manuallyStopped && (System.nanoTime() - timeStamp) <= maxTime) {
emit(false)
Timber.d("Currenttime is smaller, everything fine")
}
// This will be executed only when the Worker is running longer than maxTime
if (!manuallyStopped && !finished) {
Timber.d("Currenttime bigger, yikes. Stop worker")
emit(true)
running = false
finished = true
return#flow
} else if (finished || manuallyStopped) {
return#flow
}
}.flowOn(dispatcher)
}
How should I change my Coroutine inside my PagingSource in order to achieve my goal? Timber.d("Mediator stopped) gets called.
I appreciate every help, thank you.
Do you need to measure duration? Time is already passing everywhere, you don't need another thread or coroutine to track that. There's measureNanoTime {} that measures how long a code block took to execute.
Do you need to apply a timeout inside a suspending function? There's withTimeout exactly for that. Example:
class ShopRemoteMediator #Inject constructor(
private val db: FirebaseFirestore,
private val shopPagingWatcher: ShopPagingWatcher,
) : PagingSource<QuerySnapshot, Product>() {
#InternalCoroutinesApi
override suspend fun load(
params: LoadParams<QuerySnapshot>
): LoadResult<QuerySnapshot, Product> {
return try {
withTimeout(5, TimeUnit.SECONDS) { // <<<<<<<<<<
val currentPage = ...
val nextPage = ...
LoadResult.Page(
data = currentPage.toObjects(),
prevKey = null,
nextKey = nextPage
)
}
} catch (e: IOException) {
LoadResult.Error(e)
} catch (e: TimeoutCancellationException) { // <<<<<<<<<<
LoadResult.Error(e)
}
}
}

Flutter background task

I'm trying to run a task in backgroung (do not respond with an existing plug-in),
but on the first run I get this error "-32601 received from application: Method not found"
step by step
I have a method that sends, via MethodChannel, to the native a handle
class Backgroundtask {
static const MethodChannel _channel = const MethodChannel('backgroundtask');
static Future<bool> periodic({#required Function callback}) async {
assert(callback != null, 'task cannot be null');
CallbackHandle handle = PluginUtilities.getCallbackHandle(callback);
if (handle == null) {
return false;
}
final bool result = await _channel.invokeMethod(
'periodic',
<dynamic>[
handle.toRawHandle(),
],
);
return result ?? false;
}
}
on the other side, I start a WorkerManager, passing the handle as a Data.
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
[...]
val data = Data.Builder()
data.putLong("handle", handle)
val constrains = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val pWorkRequest = PeriodicWorkRequest
.Builder(CustomWorker::class.java, 15, TimeUnit.MINUTES)
.setInputData(data.build())
.setConstraints(constrains)
.build()
WorkManager.getInstance(context).enqueue(pWorkRequest)
[...]
}
My androidx.work.Worker.doWork()
[...]
Handler(Looper.getMainLooper()).post {
Log.d("periodic", "background service start")
val handle = inputData.getLong("handle", 0)
val flutterEngine = FlutterEngine(context)
val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(handle)
if (flutterCallback.callbackName == null) {
Log.e("periodic", "callback not found")
return
}
val assets = context.assets
val bundle = FlutterMain.findAppBundlePath()
val executor = flutterEngine.dartExecutor
val dartCallback = DartExecutor.DartCallback(assets, bundle, flutterCallback)
executor.executeDartCallback(dartCallback)
Log.d("periodic", "background service end")
}
[...]
my error occurs after the callback is executed, regardless of what it does.
I think it must be something stupid that I'm not seeing, but my head is broken.
Thank you very much in advance.
error image

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.
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!
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
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.
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)
}

Categories

Resources