David Barishev
David Barishev

Reputation: 544

Starting background task with access to the UI thread

I have been dialing with a back and forth question the i came across in my xamarin.android app.I will try to provide as much detaile as i can to make my question clear.

I have a long running task that i want to perform, when the user does clicks on a button. The task that i'm trying to run is downloading a file, and converting it to other format. The user is aware of the task, as he initiated the download, and wants it to complete.I have already written and tested the code, i just need to place it right.
Some restriction that have to be ruled:

I have had quite a debate between using Service/Intentservice/AsyncTask,and ran into couple of problems, which ONLY occured when i was actually testing on a phone.

To sum up, what should i use that fits my needs ? AsyncTask seems like the best option, but since i'm using xamarin i can use await/async, how should i structure my code ? Run all code within the activity or break it down to more compounds ?
Why did this error only occur on my phone and not on the emulator ?

Big thanks for any help! Cheers !

EDIT

From the feedback i received, i think it's best to actually use a service that will run in the foreground, i already implemented this approach, and i got into an error, which i provide more detail.

Here i'm calling my async method from inside the service, since i can't override with async, i must use Task.Factory to run an async function

 public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
 {

     Task.Factory.StartNew(async () => {
         await downloadAndConvert(id);
     });
 }

Here is the call to a class with that will provides a specific conversion. Notice the context.

 async downloadAndConvert(string id){
      ...
      VideoConverter vc = new VideoConverter();
      Java.IO.File convertedFile = await vc.ConvertFile(Application.Context,
                                        outPutFile,
                                        logFunction,
                                        progressFunction);
      // Error is firing here

      ...
 }

Here is the the call from the VideoConverter class to the library, notice the passage to of the context next.

 public async Task<File> ConvertFile(
        Context contex,
        File inputFile,
        Action<string> logger = null,
        Action<int, int> onProgress = null)
 {

 ...

 await FFMpeg.Xamarin.FFMpegLibrary.Run(
            contex,
            cmdParams
            , (s) => { ... }
            });

   ...
 }

Since the ffmpeg xamarin library is open source, i have been able to retrieve the line that causes the error.

// lets try to download
var dlg = new ProgressDialog(context);

So the error is that the library tries to make a ProgressDialog, with the passed context, but since only an activity context is able to launch the ProgressDialog its crushing on my service/application context.

The call in the FFmpeg library is to open a ProgressDialog for the first run, since it downloads the library and initializes it.I prefer not to change the library code, since it's easily available through nuget, and i don't really want to mess with building the library myself. If it's the most elegant solution than i will do it, i could just switch the ProgressDialogs for a notification and it will work fine.

Here is the complete stack trace, that i followed:

Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:143 
  at Java.Interop.JniEnvironment+InstanceMethods.CallNonvirtualVoidMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniObjectReference type, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x000a7] in /Users/builder/data/lanes/3511/0e59c362/source/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.g.cs:12083 
  at Java.Interop.JniPeerMembers+JniInstanceMethods.FinishCreateInstance (System.String constructorSignature, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x00060] in /Users/builder/data/lanes/3511/0e59c362/source/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:148 
  at Android.App.ProgressDialog..ctor (Android.Content.Context context) [0x00078] in /Users/builder/data/lanes/3511/0e59c362/source/monodroid/src/Mono.Android/platforms/android-24/src/generated/Android.App.ProgressDialog.cs:48 
  at FFMpeg.Xamarin.FFMpegLibrary+<Init>d__7.MoveNext () [0x00179] in D:\git
s-github\xamarin-ffmpeg-android\FFMpeg.Xamarin\FFMpegLibrary.cs:94 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:143 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00047] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x0002e] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x0000b] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
  at System.Runtime.CompilerServices.TaskAwaiter.GetResult () [0x00000] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:113 
  at FFMpeg.Xamarin.FFMpegLibrary+<Run>d__8.MoveNext () [0x000d0] in D:\git
s-github\xamarin-ffmpeg-android\FFMpeg.Xamarin\FFMpegLibrary.cs:191 
  --- End of managed Java.Lang.RuntimeException stack trace ---
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
    at android.os.Handler.<init>(Handler.java:200)
    at android.os.Handler.<init>(Handler.java:114)
    at android.app.Dialog.<init>(Dialog.java:119)
    at android.app.AlertDialog.<init>(AlertDialog.java:200)
    at android.app.AlertDialog.<init>(AlertDialog.java:196)
    at android.app.AlertDialog.<init>(AlertDialog.java:141)
    at android.app.ProgressDialog.<init>(ProgressDialog.java:77)

So is there any way to pass the activity context? Also, Would the application crash if I try to make a ProgressDialog when the app is closed?

Upvotes: 0

Views: 1221

Answers (3)

David Barishev
David Barishev

Reputation: 544

I Have found the reason the error is thrown.
When you first run the library, it has to download the native library and install it.It shows a progress bar in front of the application (hence why it needed an activity context).

Here is why the emulator worked, since i was using it for all my testing, it already had the library installed from previous runs, when the code where still on t the activity class.

I solved my problem by first checking if the library was installed, and if not let prompt the user to install it, since this is all on the ui thread, i had no problem passing the context.Later when the user downloaded something, it didn't need to redownload the library,so i can pass any context, because it will not use it (The check is without the context, only if the library is missing it will call the init method which he need the context to provide the prograss bar.)

I might fork the xamarin ffmpeg library and add a bit more documentation/ make the code clearer in the future.

Upvotes: 0

Kevin Krumwiede
Kevin Krumwiede

Reputation: 10308

A key principle for working with the Android framwork instead of against it is that background tasks must never outlive their hosting component. Therefore, what kind of component owns the AsyncTask is dictated by how long the AsyncTask is supposed to keep running. If you want the task to keep running when the app is "closed", then you can't host it in an Activity. This is exactly the kind of thing a Service is for. Since a Service has no UI of its own, you can provide feedback to the user via a Notification, and/or by starting an Activity when the task is complete.

Upvotes: 1

BiGGZ
BiGGZ

Reputation: 503

From what i understand, you want to execute a long-running task and still have access to the UI thread, even if the app is closed?. Well, an AsyncTask will keep running until its done(even if you close the app), and you can override the onProgressUpdate method of the AsyncTask to handle updates from doInBackground on the UI thread, but, if your AsyncTask does something on the UI-thread and the app gets closed, you may experience an exception, because your UI-thread will have been destroyed. A bit more code would be great though, to see the full context of your issue. Hope this helps.

By the way, if you get Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() from your asynctask, use the runOnUiThread(Runnable r) method for whatever UI code is causing that exception.

Upvotes: 0

Related Questions