The AsyncTask API is deprecated in Android 11. What are the alternatives?

130,822

Solution 1

private WeakReference<MyActivity> activityReference;

Good riddance that it's deprecated, because the WeakReference<Context> was always a hack, and not a proper solution.

Now people will have the opportunity to sanitize their code.


AsyncTask<String, Void, MyPojo> 

Based on this code, Progress is actually not needed, and there is a String input + MyPojo output.

This is actually quite easy to accomplish without any use of AsyncTask.

public class TaskRunner {
    private final Executor executor = Executors.newSingleThreadExecutor(); // change according to your requirements
    private final Handler handler = new Handler(Looper.getMainLooper());

    public interface Callback<R> {
        void onComplete(R result);
    }

    public <R> void executeAsync(Callable<R> callable, Callback<R> callback) {
        executor.execute(() -> {
            final R result = callable.call();
            handler.post(() -> {
                callback.onComplete(result);
            });
        });
    }
}

How to pass in the String? Like so:

class LongRunningTask implements Callable<MyPojo> {
    private final String input;

    public LongRunningTask(String input) {
        this.input = input;
    }

    @Override
    public MyPojo call() {
        // Some long running task
        return myPojo;
    }
}

And

// in ViewModel
taskRunner.executeAsync(new LongRunningTask(input), (data) -> {
    // MyActivity activity = activityReference.get();
    // activity.progressBar.setVisibility(View.GONE);
    // populateData(activity, data) ;

    loadingLiveData.setValue(false);
    dataLiveData.setValue(data);
});

// in Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.main_activity);

    viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
    viewModel.loadingLiveData.observe(this, (loading) -> {
        if(loading) {
            progressBar.setVisibility(View.VISIBLE);
        } else {
            progressBar.setVisibility(View.GONE);
        }
    });

    viewModel.dataLiveData.observe(this, (data) -> {
        populateData(data);
    }); 
}

This example used a single-threaded pool which is good for DB writes (or serialized network requests), but if you want something for DB reads or multiple requests, you can consider the following Executor configuration:

private static final Executor THREAD_POOL_EXECUTOR =
        new ThreadPoolExecutor(5, 128, 1,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

Solution 2

You can directly use Executors from java.util.concurrent package.

I also searched about it and I found a solution in this Android Async API is Deprecated post.

Unfortunately, the post is using Kotlin, but after a little effort I have converted it into Java. So here is the solution.

ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());

executor.execute(new Runnable() {
    @Override
    public void run() {

        //Background work here

        handler.post(new Runnable() {
            @Override
            public void run() {
                //UI Thread work here
            }
        });
    }
});

Pretty simple right? You can simplify it little more if you are using Java 8 in your project.

ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());

executor.execute(() -> {
    //Background work here
    handler.post(() -> {
        //UI Thread work here
    });
});

Still, it cannot defeat kotlin terms of conciseness of the code, but better than the previous java version.

Hope this will help you. Thank You

Solution 3

One of the simplest alternative is to use Thread

new Thread(new Runnable() {
    @Override
    public void run() {
        // do your stuff
        runOnUiThread(new Runnable() {
            public void run() {
                // do onPostExecute stuff
            }
        });
    }
}).start();

If your project supports JAVA 8, you can use lambda:

new Thread(() -> {
    // do background stuff here
    runOnUiThread(()->{
        // OnPostExecute stuff here
    });
}).start();

Solution 4

According to the Android documentation AsyncTask was deprecated in API level 30 and it is suggested to use the standard java.util.concurrent or Kotlin concurrency utilities instead.

Using the latter it can be achieved pretty simple:

  1. Create generic extension function on CoroutineScope:

     fun <R> CoroutineScope.executeAsyncTask(
             onPreExecute: () -> Unit,
             doInBackground: () -> R,
             onPostExecute: (R) -> Unit
     ) = launch {
         onPreExecute() // runs in Main Thread
         val result = withContext(Dispatchers.IO) { 
             doInBackground() // runs in background thread without blocking the Main Thread
         }
         onPostExecute(result) // runs in Main Thread
     } 
    
  2. Use the function with any CoroutineScope which has Dispatchers.Main context:

    • In ViewModel:

      class MyViewModel : ViewModel() {
      
          fun someFun() {
              viewModelScope.executeAsyncTask(onPreExecute = {
                  // ... runs in Main Thread
              }, doInBackground = {
                  // ... runs in Worker(Background) Thread
                  "Result" // send data to "onPostExecute"
              }, onPostExecute = {
                  // runs in Main Thread
                  // ... here "it" is the data returned from "doInBackground"
              })
          }
      }
      
    • In Activity or Fragment:

      lifecycleScope.executeAsyncTask(onPreExecute = {
          // ... runs in Main Thread
      }, doInBackground = {
          // ... runs in Worker(Background) Thread
          "Result" // send data to "onPostExecute"
      }, onPostExecute = {
          // runs in Main Thread
          // ... here "it" is the data returned from "doInBackground"
      })
      

    To use viewModelScope or lifecycleScope add next line(s) to dependencies of the app's build.gradle file:

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION" // for viewModelScope
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$LIFECYCLE_VERSION" // for lifecycleScope
    

    At the time of writing final LIFECYCLE_VERSION = "2.3.0-alpha05"

UPDATE:

Also we can implement progress updating using onProgressUpdate function:

fun <P, R> CoroutineScope.executeAsyncTask(
        onPreExecute: () -> Unit,
        doInBackground: suspend (suspend (P) -> Unit) -> R,
        onPostExecute: (R) -> Unit,
        onProgressUpdate: (P) -> Unit
) = launch {
    onPreExecute()

    val result = withContext(Dispatchers.IO) {
        doInBackground {
            withContext(Dispatchers.Main) { onProgressUpdate(it) }
        }
    }
    onPostExecute(result)
}

Using any CoroutineScope (viewModelScope/lifecycleScope, see implementations above) with Dispatchers.Main context we can call it:

someScope.executeAsyncTask(
    onPreExecute = {
        // ... runs in Main Thread
    }, doInBackground = { publishProgress: suspend (progress: Int) -> Unit ->
        
        // ... runs in Background Thread

        // simulate progress update
        publishProgress(50) // call `publishProgress` to update progress, `onProgressUpdate` will be called
        delay(1000)
        publishProgress(100)

        
        "Result" // send data to "onPostExecute"
    }, onPostExecute = {
        // runs in Main Thread
        // ... here "it" is a data returned from "doInBackground"
    }, onProgressUpdate = {
        // runs in Main Thread
        // ... here "it" contains progress
    }
)

Solution 5

Use this class to execute background task in Background Thread this class is work for all android API version include Android 11 also this code is same work like AsyncTask with doInBackground and onPostExecute methods

public abstract class BackgroundTask {

    private Activity activity;
    public BackgroundTask(Activity activity) {
        this.activity = activity;
    }

    private void startBackground() {
        new Thread(new Runnable() {
            public void run() {

                doInBackground();
                activity.runOnUiThread(new Runnable() {
                    public void run() {

                        onPostExecute();
                    }
                });
            }
        }).start();
    }
    public void execute(){
        startBackground();
    }

    public abstract void doInBackground();
    public abstract void onPostExecute();

}

After copying the above class, you can then use it with this:

new BackgroundTask(MainActivity.this) {
        @Override
        public void doInBackground() {

            //put you background code
            //same like doingBackground
            //Background Thread
        }

        @Override
        public void onPostExecute() {

            //hear is result part same
            //same like post execute
            //UI Thread(update your UI widget)
        }
    }.execute();
Share:
130,822
Zeeshan
Author by

Zeeshan

Updated on July 08, 2022

Comments

  • Zeeshan
    Zeeshan almost 2 years

    Google is deprecating Android AsyncTask API in Android 11 and suggesting to use java.util.concurrent instead. you can check out the commit here

     *
     * @deprecated Use the standard <code>java.util.concurrent</code> or
     *   <a href="https://developer.android.com/topic/libraries/architecture/coroutines">
     *   Kotlin concurrency utilities</a> instead.
     */
    @Deprecated
    public abstract class AsyncTask<Params, Progress, Result> {
    

    If you’re maintaining an older codebase with asynchronous tasks in Android, you’re likely going to have to change it in future. My question is that what should be proper replacement of the code snippet shown below using java.util.concurrent. It is a static inner class of an Activity. I am looking for something that will work with minSdkVersion 16

    private static class LongRunningTask extends AsyncTask<String, Void, MyPojo> {
            private static final String TAG = MyActivity.LongRunningTask.class.getSimpleName();
            private WeakReference<MyActivity> activityReference;
    
            LongRunningTask(MyActivity context) {
                activityReference = new WeakReference<>(context);
            }
    
            @Override
            protected MyPojo doInBackground(String... params) {
                // Some long running task
                
            }
    
            @Override
            protected void onPostExecute(MyPojo data) {
    
                MyActivity activity = activityReference.get();
                activity.progressBar.setVisibility(View.GONE);
                populateData(activity, data) ;
            }     
    
    
        }
    
    • CommonsWare
      CommonsWare over 4 years
      "Deprecated" means that Google is recommending that you move to something else. It does not mean that the class will be removed any time soon. In particular, AsyncTask cannot be removed without breaking backwards compatibility.
    • Style-7
      Style-7 over 4 years
    • EpicPandaForce
      EpicPandaForce over 4 years
      @Style-7 it is not.
    • beroal
      beroal over 4 years
      There is no such thing as a "static inner class". You mean a static nested class.
    • Kimi Chiu
      Kimi Chiu about 3 years
      This is a disaster. It's recommended to use AsyncTask from official Android Document. I was a backend developer, already familiar with the executorService. For this recommendation, I migrated all background tasks to use AsyncTask. And now they tell us not to use it?
    • Duna
      Duna almost 3 years
      @CommonsWare starting with Android 11 Google started deleting deprecated methods.
    • CommonsWare
      CommonsWare almost 3 years
      @Duna: Got any examples? They delete deprecated methods from libraries, as developers control the versions of libraries that they use. But, as I noted, AsyncTask cannot be removed without breaking backwards compatibility.
    • Addy
      Addy over 2 years
      @CommonsWare yes it is working fine in android 11 and i'm going to use till it is working or i should move to alternative asap?
    • CommonsWare
      CommonsWare over 2 years
      @Addy: The specific concern that I commented on here is AsyncTask being deleted, and that cannot happen without breaking lots of existing apps. Programmers should learn other techniques than AsyncTask (RxJava, Kotlin coroutines, etc.) simply because they are better and at this point are used more widely in professional settings.
    • metvsk
      metvsk over 2 years
    • Yohannes Masterous
      Yohannes Masterous over 2 years
  • Kris B
    Kris B over 4 years
    I am getting an error on executor.post. Cannot resolve method
  • EpicPandaForce
    EpicPandaForce over 4 years
    @KrisB apparently it's called execute() instead of post()
  • Kris B
    Kris B over 4 years
    Do you know if it's ok to pass a Context into this? I know that passing a Context into AsyncTask was one of it's issues.
  • EpicPandaForce
    EpicPandaForce over 4 years
    You still need to run this in a ViewModel, retained fragment, singleton, or whatever else, as you normally would with anything async in Android.
  • EpicPandaForce
    EpicPandaForce over 4 years
    For progress update support, you can check this Kotlin example: reddit.com/r/androiddev/comments/dt2kbh/…
  • Kris B
    Kris B over 4 years
    Thanks, I replaced all my AsyncTasks with your code. Do you know if I should be using newSingleThreadExecutor for all async operations or is one better for database reads vs. writers?
  • EpicPandaForce
    EpicPandaForce over 4 years
    newSingleThreadExecutor is better for writes, but you should definitely use the THREAD_POOL_EXECUTOR at the end of the post for database reads.
  • Tenfour04
    Tenfour04 over 4 years
    The WeakReference hack isn't inherently tied to AsyncTask, though, is it? The Runnable someone passes to your TaskRunner.executeAsync() is likely to capture an Activity/Fragment reference and leak it if not dealt with. Someone might try the same WeakReference hack to get around that instead of properly supporting cancellation. In the same way AsyncTask could be used in a more proper way, supporting cancellation and being actively cancelled by the user in onDestroy(). Though I'm not complaining about them deprecating such a clumsy tool.
  • EpicPandaForce
    EpicPandaForce over 4 years
    That's why the sample I provided runs it in the ViewModel instead 😏 although yes, what I wrote doesn't cancel the task. You could handle it with interruptions if needed, but it's almost easier to run the async task in a singleton global context and use event emission (just like how ViewModel communicates one-off events to its view)
  • k2col
    k2col about 4 years
    In TaskRunner I get a compiler error "Unhandled Exception java.lang.Exception` at callable.call() ... what's the best way to handle this?
  • Sam
    Sam about 4 years
    Is there a built-in equivalent to TaskRunner in Android or AndroidX? It looks like a neat way to do it but seems strange that something so important isn't built-in.
  • EpicPandaForce
    EpicPandaForce about 4 years
    @Sam the built-in version was called AsyncTask and it was so bad it got deprecated :p
  • Jones
    Jones about 4 years
    How to know if my task is finished or not?
  • EpicPandaForce
    EpicPandaForce about 4 years
    You get a callback when it's done. If you didn't get a callback, then it's not yet done.
  • dontangg
    dontangg about 4 years
    I really appreciated this example. Thanks! I ended up using this almost exactly as is. I used a static executor like you showed in your code sample at the very end but still used Executors.newSingleThreadExecutor().
  • Jones
    Jones about 4 years
    How can I cancel an Task which is Executed this way (and interrupt it)?
  • EpicPandaForce
    EpicPandaForce about 4 years
    If you do need cancelation, then instead of executor.execute, you have to use executor.submit and cancel the future. baeldung.com/java-future
  • Peter
    Peter almost 4 years
    Do you have any suggestions on how to implement the onProgressUpdate as well using the kotlin coroutines?
  • Paula Livingstone
    Paula Livingstone almost 4 years
    Oh god, so it turns out Handler is now deprecated also.....
  • EpicPandaForce
    EpicPandaForce almost 4 years
    That can't be right. android.os.Handler is essential for communicating to the UI thread or any other HandlerThreads. Only the no-arg constructor seems to be deprecated, Handler(Looper.getMainLooper()) seems to be legit.
  • Ahamadullah Saikat
    Ahamadullah Saikat over 3 years
    How to show percentage when background stuff called?
  • mayank1513
    mayank1513 over 3 years
    You need to use runOnUiThread to update your progress-bar or any other mechanism you are using for updating / displaying % of task completed.
  • Darksymphony
    Darksymphony over 3 years
    no kotlin please, first use Java, then maybe Kotlin as an alternative for those who are using it. Thanks
  • Branddd
    Branddd over 3 years
    I heard Coroutines had "easy entry". Does that mean its easy to breach?
  • Son Truong
    Son Truong over 3 years
    I feel like this solution is a simple version of the AsyncTask, because AsyncTask uses Executor and Handler under the hood and provides other features such as cancel, update progress, etc.
  • Son Truong
    Son Truong over 3 years
    This solution has several drawbacks. First, the thread keeps a reference to the activity, this might leak the context and crash the app. Second, we cannot use this from a fragment. Third, we cannot update progress of the background task, Fourth, there is no way to cancel the thread. Finally, it creates many boilerplate code in the app.
  • drmrbrewer
    drmrbrewer over 3 years
    So why are we deprecating AsyncTask only to re-create it from scratch, and still follow apparently bad habits with memory leaks? If I need to hold a reference to an Activity for use after my async task completes (using whatever async mechanism) then surely I am still going to need a WeakReference to the Activity, just as before? I don't see what is gained by using something that is not AsyncTask but basically is...
  • EpicPandaForce
    EpicPandaForce over 3 years
    You've never once needed a WeakReference, you needed non-config instance scope (retained fragment scope, now ViewModel scope) and observer pattern for pending values. You only need weak ref if you're building a reactive data layer, and even then only if you're not building on top of lifecycle awareness or structured concurrency.
  • drmrbrewer
    drmrbrewer over 3 years
    But the use of WeakReference isn't connected specifically to the use of AsyncTask, so your opening statement "Good riddance that it [AsyncTask] is deprecated, because the WeakReference<Context> was always a hack, and not a proper solution" isn't really relevant? Anyway, I've now used your TaskRunner to eliminate AsyncTask entirely from my code, and maybe it does seem a bit cleaner overall, which is good, but nothing has fundamentally changed... in particular I still use WeakReference just as I did before, rather proving the point. Maybe that is a task for the next cleanup. Thanks!
  • Pankaj Kumar
    Pankaj Kumar over 3 years
    @Darksymphony I completely disagree with you, question is old in terms of using Java. If you are still using Java for Android, you need to rethink your choice. He wrote a very good alternative for AsynTask.
  • Darksymphony
    Darksymphony over 3 years
    hopefully Java will stay for next X years as the basic language for android. Someone lazy came with Kotlin and forced it to android devs with it's funny commands :) Maybe one day I will rethink. But as long as we have a choice, I'll stay with Java
  • The incredible Jan
    The incredible Jan over 3 years
    @Darksymphony Where is the problem to write some parts with Kotlin and some with JAVA? Should work without any real problems.
  • The incredible Jan
    The incredible Jan over 3 years
    Migrate from AsyncTask to AsyncTask?
  • Darksymphony
    Darksymphony over 3 years
    @TheincredibleJan yes you are right, but works not without including Kotlin dependencies
  • Prashant
    Prashant over 3 years
    How do I publish progress ?
  • EpicPandaForce
    EpicPandaForce over 3 years
  • Adam Burley
    Adam Burley over 3 years
    Your code does not even compile....I think has some typos in, for example when you say new Runnable({...}) you mean new Runnable(){...}. Because the first one is like you call a constructor and pass an array initializer, triggering a compiler error. the second one is the proper way to create anonymous inner classes
  • quilkin
    quilkin over 3 years
    I tried this and the app just drops out (with no errors in logcat) as soon as execute() is called. Tried both types of Executor. Mind you, I'm calling from an Activity rather than from a ViewModel. Is this the reason?
  • EpicPandaForce
    EpicPandaForce over 3 years
    I'd have to see the code and the exception stack trace to tell you
  • Faisal Khan
    Faisal Khan over 3 years
    @Son Truong I have solutions for three of your four problems. First, WeakReference can be used to prevent leak. Second, you can absolutely use this inside fragments via getActivity(). Fourth, you can cancel at any time using Executer and Future.
  • Adnan haider
    Adnan haider over 3 years
    have this solution available in java?
  • Sergio
    Sergio over 3 years
    @Adnanhaider I am afraid it is not.
  • Sergio
    Sergio about 3 years
    @Undefinedfunction executeAsyncTask is an extension function on CoroutineScope, you can create your own CoroutineScope and call executeAsyncTask on it.
  • Undefined function
    Undefined function about 3 years
    I tried to update the UI in onPostExecute but what I get is error ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. which seems that its not running in UI thread
  • Undefined function
    Undefined function about 3 years
    Great this work really well with non context class but how will you cancel it or get a progress?
  • Sergio
    Sergio about 3 years
    @Undefinedfunction please try to use MainScope (kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core‌​/…) to call executeAsyncTask method: mainScope.executeAsyncTask ()
  • Undefined function
    Undefined function about 3 years
    The solution is really good however I think this approach means that your async task will be always inside ViewModel, Activity, or Fragment not in just regular class for example that only implements an interface.
  • Undefined function
    Undefined function about 3 years
    There are some cases when you really need a WeakReference, an example would be when working with a local library that accept a View parameter to do some fetching from network and update it. Since it is not part of your app module directly there is no way you can apply its changes without moving those classes to your app module itself. I would rather say that as much as possible avoid using WeakReference as it means you have a bad structure but no total restriction is imposed. After all they still exist until today and I see no Android documentation against using it.
  • Sergio
    Sergio about 3 years
    To call executeAsyncTask we need some CoroutineScope with Dispatchers.Main context to run appropriate methods(onPreExecute()...) on the Main Thread. You can create a CoroutineScope with Dispatchers.Main context in any class.
  • chitgoks
    chitgoks about 3 years
    This looks good. Lacking one thing though. possible to cancel it?
  • Smack Alpha
    Smack Alpha about 3 years
    @Idan Damri, Your Explanation is awesome. Helped me a lot to achieve like async task with less code in kotlin
  • Idan Damri
    Idan Damri almost 3 years
    @chitgoks someone asked in the comments and I've answered it. Check it out =]
  • arun
    arun almost 3 years
    Actually you can go one more step down: Executors.newSingleThreadExecutor().execute(() -> dao.insert(data));
  • Benjamin Basmaci
    Benjamin Basmaci almost 3 years
    @SonTruong I have a few genuine questions to those drawbacks. 1: How/Why does the thread keep a reference of the activity, if its not specifically passed? I understand runOnUiThread, but for short tasks, this shouldn't be a problem, no? 3: Can't the progress of the background task simply be handled by a respective call within runOnUiThread, just like publishProgress/onProgressionUpdate? 4: Going Down AsyncTask and FutureTask code, all it does is use the Thread.interrupt functionality to create the cancel functionality. Wouldnt it be possible to do the same with this approach?
  • Duna
    Duna almost 3 years
    Great example but instead of GlobalScope I would recommend to use CoroutineContext
  • Mikhail Vasilyev
    Mikhail Vasilyev almost 3 years
    Your usage of mIsInterrupted is not thread-safe. Either it must be made atomic/volatile or the methods using it must be synchronized.
  • BeLambda
    BeLambda almost 3 years
    @Darksymphony Kotlin is the superior language, no question. You have to adapt to the times.
  • Islam Alshnawey
    Islam Alshnawey almost 3 years
    why you still name your solution with async task , even you use ExecutorService .
  • Attaullah
    Attaullah almost 3 years
    Just for simplicity for the beginner
  • Sourav Kannantha B
    Sourav Kannantha B almost 3 years
    @EpicPandaForce in that LongRunningTask, how do you get the reference to a context?
  • Sourav Kannantha B
    Sourav Kannantha B almost 3 years
    @EpicPandaForce can you please review this.
  • Vivek Thummar
    Vivek Thummar over 2 years
    I have one class which have AsyncTask like this - new AsyncTask<Integer, Void, Float>() {...} means directly, it's not like i'm calling it from somewhere, so what about params from doInBackground??, for more info look at this - github.com/sharish/ScratchView/blob/master/views/src/main/ja‌​va/…
  • EpicPandaForce
    EpicPandaForce over 2 years
    @VivekThummar see LongRunningTask in the answer
  • Patrick
    Patrick over 2 years
    In the above code from user Attaullah, is it possible to assign a specific name to each background task that is created through that class? That would make it easier to analyse the background tasks in the profiler of Android studio.
  • Attaullah
    Attaullah over 2 years
    Yes, possible you can easily give it a name.
  • Patrick
    Patrick over 2 years
    I've added "Thread.currentThread().setName(threadName);" just below "public void run()" and I'm passing threadName through the Execute-method. That's working perfect.
  • Patrick
    Patrick over 2 years
    Another question on your code: How do you call the Shutdown-method in your code? Do you call it through "private AsyncTasks mTask;" -> "mTask = new AsyncTasks();" -> "mTask.execute();" and right afterwards "mTask.shutdown();"?
  • Attaullah
    Attaullah over 2 years
    Yes first check if is running then try to shutdown. if (!isShutdown()) mTask.shutdown();
  • Kroi
    Kroi over 2 years
    Thank you for this! It helped me a lot when calling server API calls. But what if I want to show some updating progress bars? where can I put the onProgressUpdate part?
  • Bhavna
    Bhavna over 2 years
    @Vitaly Can you please add the implementation for onProgressUpdate also
  • pavi2410
    pavi2410 over 2 years
    @Kroi you'd have to call handler.post everytime you want to post an update to main thread
  • JP711
    JP711 over 2 years
    How to stop the execution of tasks?, for example when closing the activity that has called it, etc. In my case I use it in a word search engine of my database, I want that when changing the text of an EditText it stops the current query and starts a new one, but I can't find a way to stop the ExecutorService.
  • Idan Damri
    Idan Damri about 2 years
    @SmackAlpha thss as no you 🙏🏻
  • drmrbrewer
    drmrbrewer about 2 years
    Is there a version of this with an in-built timeout feature?
  • Fahad-Android
    Fahad-Android about 2 years
    This solution saves the day! Ton thanks brooo