Search code examples
androidandroid-asynctaskandroid-serviceandroid-intentservice

How to queue multiple tasks in a foreground service, so that they execute one by one?


public class CopyService extends Service {

private List<CustomFile> taskList;
private AsyncTask fileTask;

@Override
public void onCreate() {
    super.onCreate();
    taskList = new ArrayList<>();
    fileTask = new fileTaskAsync();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

    String filePath = intent.getStringExtra("filePath");
    String fileType = intent.getStringExtra("fileType");
    String taskType = intent.getStringExtra("taskType");
    String fileName = intent.getStringExtra("fileName");

    CustomFile customFile = new CustomFile();
    customFile.filePath = filePath;
    customFile.fileType = fileType;
    customFile.taskType = taskType;
    customFile.fileName = fileName;
    taskList.add(customFile);

    Notification notification = getNotification();

    startForeground(787, notification);

    if (fileTask.getStatus() != AsyncTask.Status.RUNNING) {

        CustomFile current = taskList.get(0);
        taskList.remove(current);

        fileTask = new fileTaskAsync().execute(current);
    }

    stopSelf();
    return START_NOT_STICKY;
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
    return null;
}

private class fileTaskAsync extends AsyncTask<CustomFile, Void, String> {

    @Override
    protected String doInBackground(CustomFile... customFiles) {

        CustomFile customFile = customFiles[0];

        FileUtils.doFileTask(customFile.filePath, customFile.fileType,
                customFile.taskType);

        return customFile.fileName;
    }

    @Override
    protected void onPostExecute(String name) {
        sendResult(name);

        if (!taskList.isEmpty()) {
            CustomFile newCurrent = taskList.get(0);
            taskList.remove(newCurrent);
            fileTask = new fileTaskAsync().execute(newCurrent);
        }
    }
}

private void sendResult(String name) {
    Intent intent = new Intent("taskStatus");
    intent.putExtra("taskName", name);
    LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}

}

I need to execute multiple tasks in a service one by one. Task is either copying or moving local files. Suppose, user is copying a big file and he wants to copy or move other files. I need the subsequent tasks to be queued and exected one by one.

Currently, I'm creating a list inside the service and running an async task. In onPostExecute, I check for remaining tasks in the list and start the async task again from there. As shown in the code.

But, I'm concerned about memory leaks. And I'm very new to programming so, I don't know what's the best practice in such situations.

I can't use IntentService, because I want the task to continue even if the user hits home button to open some other app.


Solution

  • AS I said in the comments, I think your solution is reasonable. A Foreground Service is a good candidate for long running work that needs to be executed immediately, and from your description your file copying task matches that criteria.

    That said, I don't believe AsyncTask is a good candidate for your problem. AsyncTasks are best deployed when you need to do some quick work off the main thread, in the order of a few hundred milliseconds at most, whereas your copy task could presumably take several seconds.

    As you have multiple tasks to complete which aren't directly dependent on one another, I would recommend you make use of a thread pool to conduct this work. For that you can use an ExecutorService:

    public class CopyService extends Service {
    
    private final Deque<CustomFile> tasks = new ArrayDeque<>();
    private final Deque<Future<?>> futures = new LinkedBlockingDequeue<>();
    private final ExecutorService executor = Executors.newCachedThreadPool();
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    
        //May as well add a factory method to your CustomFile that creates one from an Intent
        CustomFile customFile = CustomFile.fromIntent(intent);
        tasks.offer(customFile);
    
        //...Add any other tasks to this queue...
    
        Notification notification = getNotification();
    
        startForeground(787, notification);
    
        for(CustomFile file : tasks) {
           final Future<?> future = executor.submit(new Runnable() {
               @Override
               public void run() {
                   final CustomFile file = tasks.poll();
                   //Ddo work with the file...
                   LocalBroadcastManager.getInstance(CopyService.this).sendBroadcast(...);
                   //Check to see whether we've now executed all tasks. If we have, kill the Service.
                   if(tasks.isEmpty()) stopSelf();
               }
           });
           futures.offer(future);
        }
        return START_NOT_STICKY;
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        //Clear pending and active work if the Service is being shutdown
        //You may want to think about whether you want to reschedule any work here too
        for(Future<?> future : futures) {
            if(!future.isDone() && !future.isCancelled()) {
                future.cancel(true); //May pass "false" here. Terminating work immediately may produce side effects.
            } 
        }
    }
    
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    

    This shouldn't cause any memory leaks, as any pending work is destroyed along with the Service.