Download multiple files with a progress bar in ListView Android

I want to create a ListView that allows the user to download many files and show a progress bar status in each ListView item. It looks like this:

enter image description here

The download ListView has some rules:

  • Each download task displays in one ListView item with a progress bar, percentage and current status (downloading, waiting, finished).
  • Allow a maximum of 5 download tasks, other tasks have to wait
  • Users can cancel the download task, and remove this task from the ListView
  • The ListView is in the download activity. Users can change to another activity. When they leave, continue downloading in the background. When they come back, display the current progress and status of the download tasks.
  • Keep the completed download tasks until users want to remove them.

I tried to use the ThreadPoolExcutor in a Service. For each download task, I can get the percentage complete, but I don't know how to broadcast them to an adapter to display progress. And I don't know how to keep all the tasks running in the background and then post progress when the activity containing the ListView is active and keep the completed tasks
It would be great if there were any library or example which can solve my problems. Thanks in advance! P/S: I have already searched similar questions, but I can't find the solution for this one, so I had to create my own question in detail. So please don't mark it as duplicate. Thanks.


This is a working sample, take a look.

Launch the app, press back button and then again come back to test the case of launching another Activity and coming back.

Make sure to get PARTIAL_WAKE_LOCK for your IntentService to ensure that CPU keeps running.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.Activity;
import android.app.IntentService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

public class MainActivity extends Activity {
    public static final String ID = "id";
    private ListView mListView;
    private ArrayAdapter<File> mAdapter;
    private boolean mReceiversRegistered;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = mListView = (ListView) findViewById(R.id.list);
        long id = 0;
        File[] files = {getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id++),
                getFile(id++), getFile(id++), getFile(id)};
        listView.setAdapter(mAdapter = new ArrayAdapter<File>(this,
                R.layout.row, R.id.textView, files) {
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                View v = super.getView(position, convertView, parent);
                updateRow(getItem(position), v);
                return v;
            }
        });

        if (savedInstanceState == null) {
            Intent intent = new Intent(this, DownloadingService.class);
            intent.putParcelableArrayListExtra("files", new ArrayList<File>(Arrays.asList(files)));
            startService(intent);
        }

        registerReceiver();
    }

    private File getFile(long id) {
        return new File(id, "https://someurl/" + id);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver();
    }

    private void registerReceiver() {
        unregisterReceiver();
        IntentFilter intentToReceiveFilter = new IntentFilter();
        intentToReceiveFilter
                .addAction(DownloadingService.PROGRESS_UPDATE_ACTION);
        LocalBroadcastManager.getInstance(this).registerReceiver(
                mDownloadingProgressReceiver, intentToReceiveFilter);
        mReceiversRegistered = true;
    }

    private void unregisterReceiver() {
        if (mReceiversRegistered) {
            LocalBroadcastManager.getInstance(this).unregisterReceiver(
                    mDownloadingProgressReceiver);
            mReceiversRegistered = false;
        }
    }

    private void updateRow(final File file, View v) {
        ProgressBar bar = (ProgressBar) v.findViewById(R.id.progressBar);
        bar.setProgress(file.progress);
        TextView tv = (TextView) v.findViewById(R.id.textView);
        tv.setText(file.toString());
        v.findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent();
                i.setAction(DownloadingService.ACTION_CANCEL_DOWNLOAD);
                i.putExtra(ID, file.getId());
                LocalBroadcastManager.getInstance(MainActivity.this).sendBroadcast(i);
            }
        });
    }

    // don't call notifyDatasetChanged() too frequently, have a look at
    // following url http://stackoverflow.com/a/19090832/1112882
    protected void onProgressUpdate(int position, int progress) {
        final ListView listView = mListView;
        int first = listView.getFirstVisiblePosition();
        int last = listView.getLastVisiblePosition();
        mAdapter.getItem(position).progress = progress > 100 ? 100 : progress;
        if (position < first || position > last) {
            // just update your data set, UI will be updated automatically in next
            // getView() call
        } else {
            View convertView = mListView.getChildAt(position - first);
            // this is the convertView that you previously returned in getView
            // just fix it (for example:)
            updateRow(mAdapter.getItem(position), convertView);
        }
    }

    protected void onProgressUpdateOneShot(int[] positions, int[] progresses) {
        for (int i = 0; i < positions.length; i++) {
            int position = positions[i];
            int progress = progresses[i];
            onProgressUpdate(position, progress);
        }
    }

    private final BroadcastReceiver mDownloadingProgressReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(
                    DownloadingService.PROGRESS_UPDATE_ACTION)) {
                final boolean oneShot = intent
                        .getBooleanExtra("oneshot", false);
                if (oneShot) {
                    final int[] progresses = intent
                            .getIntArrayExtra("progress");
                    final int[] positions = intent.getIntArrayExtra("position");
                    onProgressUpdateOneShot(positions, progresses);
                } else {
                    final int progress = intent.getIntExtra("progress", -1);
                    final int position = intent.getIntExtra("position", -1);
                    if (position == -1) {
                        return;
                    }
                    onProgressUpdate(position, progress);
                }
            }
        }
    };

    public static class DownloadingService extends IntentService {
        public static String PROGRESS_UPDATE_ACTION = DownloadingService.class
                .getName() + ".progress_update";

        private static final String ACTION_CANCEL_DOWNLOAD = DownloadingService.class
                .getName() + "action_cancel_download";

        private boolean mIsAlreadyRunning;
        private boolean mReceiversRegistered;

        private ExecutorService mExec;
        private CompletionService<NoResultType> mEcs;
        private LocalBroadcastManager mBroadcastManager;
        private List<DownloadTask> mTasks;

        private static final long INTERVAL_BROADCAST = 800;
        private long mLastUpdate = 0;

        public DownloadingService() {
            super("DownloadingService");
            mExec = Executors.newFixedThreadPool( /* only 5 at a time */5);
            mEcs = new ExecutorCompletionService<NoResultType>(mExec);
            mBroadcastManager = LocalBroadcastManager.getInstance(this);
            mTasks = new ArrayList<MainActivity.DownloadingService.DownloadTask>();
        }

        @Override
        public void onCreate() {
            super.onCreate();
            registerReceiver();
        }

        @Override
        public void onDestroy() {
            super.onDestroy();
            unregisterReceiver();
        }

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if (mIsAlreadyRunning) {
                publishCurrentProgressOneShot(true);
            }
            return super.onStartCommand(intent, flags, startId);
        }

        @Override
        protected void onHandleIntent(Intent intent) {
            if (mIsAlreadyRunning) {
                return;
            }
            mIsAlreadyRunning = true;

            ArrayList<File> files = intent.getParcelableArrayListExtra("files");
            final Collection<DownloadTask> tasks = mTasks;
            int index = 0;
            for (File file : files) {
                DownloadTask yt1 = new DownloadTask(index++, file);
                tasks.add(yt1);
            }

            for (DownloadTask t : tasks) {
                mEcs.submit(t);
            }
            // wait for finish
            int n = tasks.size();
            for (int i = 0; i < n; ++i) {
                NoResultType r;
                try {
                    r = mEcs.take().get();
                    if (r != null) {
                        // use you result here
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
            // send a last broadcast
            publishCurrentProgressOneShot(true);
            mExec.shutdown();
        }

        private void publishCurrentProgressOneShot(boolean forced) {
            if (forced
                    || System.currentTimeMillis() - mLastUpdate > INTERVAL_BROADCAST) {
                mLastUpdate = System.currentTimeMillis();
                final List<DownloadTask> tasks = mTasks;
                int[] positions = new int[tasks.size()];
                int[] progresses = new int[tasks.size()];
                for (int i = 0; i < tasks.size(); i++) {
                    DownloadTask t = tasks.get(i);
                    positions[i] = t.mPosition;
                    progresses[i] = t.mProgress;
                }
                publishProgress(positions, progresses);
            }
        }

        private void publishCurrentProgressOneShot() {
            publishCurrentProgressOneShot(false);
        }

        private synchronized void publishProgress(int[] positions,
                                                  int[] progresses) {
            Intent i = new Intent();
            i.setAction(PROGRESS_UPDATE_ACTION);
            i.putExtra("position", positions);
            i.putExtra("progress", progresses);
            i.putExtra("oneshot", true);
            mBroadcastManager.sendBroadcast(i);
        }

        // following methods can also be used but will cause lots of broadcasts
        private void publishCurrentProgress() {
            final Collection<DownloadTask> tasks = mTasks;
            for (DownloadTask t : tasks) {
                publishProgress(t.mPosition, t.mProgress);
            }
        }

        private synchronized void publishProgress(int position, int progress) {
            Intent i = new Intent();
            i.setAction(PROGRESS_UPDATE_ACTION);
            i.putExtra("progress", progress);
            i.putExtra("position", position);
            mBroadcastManager.sendBroadcast(i);
        }

        class DownloadTask implements Callable<NoResultType> {
            private int mPosition;
            private int mProgress;
            private boolean mCancelled;
            private final File mFile;
            private Random mRand = new Random();

            public DownloadTask(int position, File file) {
                mPosition = position;
                mFile = file;
            }

            @Override
            public NoResultType call() throws Exception {
                while (mProgress < 100 && !mCancelled) {
                    mProgress += mRand.nextInt(5);
                    Thread.sleep(mRand.nextInt(500));

                    // publish progress
                    publishCurrentProgressOneShot();

                    // we can also call publishProgress(int position, int
                    // progress) instead, which will work fine but avoid broadcasts
                    // by aggregating them

                    // publishProgress(mPosition,mProgress);
                }
                return new NoResultType();
            }

            public int getProgress() {
                return mProgress;
            }

            public int getPosition() {
                return mPosition;
            }

            public void cancel() {
                mCancelled = true;
            }
        }


        private void registerReceiver() {
            unregisterReceiver();
            IntentFilter filter = new IntentFilter();
            filter.addAction(DownloadingService.ACTION_CANCEL_DOWNLOAD);
            LocalBroadcastManager.getInstance(this).registerReceiver(
                    mCommunicationReceiver, filter);
            mReceiversRegistered = true;
        }

        private void unregisterReceiver() {
            if (mReceiversRegistered) {
                LocalBroadcastManager.getInstance(this).unregisterReceiver(
                        mCommunicationReceiver);
                mReceiversRegistered = false;
            }
        }

        private final BroadcastReceiver mCommunicationReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals(
                        DownloadingService.ACTION_CANCEL_DOWNLOAD)) {
                    final long id = intent.getLongExtra(ID, -1);
                    if (id != -1) {
                        for (DownloadTask task : mTasks) {
                            if (task.mFile.getId() == id) {
                                task.cancel();
                                break;
                            }
                        }
                    }
                }
            }
        };

        class NoResultType {
        }
    }

    public static class File implements Parcelable {
        private final long id;
        private final String url;
        private int progress;

        public File(long id, String url) {
            this.id = id;
            this.url = url;
        }

        public long getId() {
            return id;
        }

        public String getUrl() {
            return url;
        }

        @Override
        public String toString() {
            return url + " " + progress + " %";
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeLong(this.id);
            dest.writeString(this.url);
            dest.writeInt(this.progress);
        }

        private File(Parcel in) {
            this.id = in.readLong();
            this.url = in.readString();
            this.progress = in.readInt();
        }

        public static final Parcelable.Creator<File> CREATOR = new Parcelable.Creator<File>() {
            public File createFromParcel(Parcel source) {
                return new File(source);
            }

            public File[] newArray(int size) {
                return new File[size];
            }
        };
    }
}

row.xml layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Title" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100" />

    <Button
        android:id="@+id/cancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Cancel" />

</LinearLayout>

activity_main.xml just contains a ListView:

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Note: Make sure to register DownloadingService in AndroidManifest.xml like this:

<service android:name=".MainActivity$DownloadingService" />

UPDATE:

  • Cancel support added

IMO - 4 things you would want to implement:

  • Listener/Wrapper for HttpClient.Get.Exec() - so you know how many bytes received on each write()

    Broadcast/receive for listener that u mention may actually be redundant

    Async HttpClient - nonblocking is a MUST

    Pooling/Queueing for requests

for the Listener you can see the observer pattern and switch the 'upload' to down.

since you already have an observer pattern you should be able to adapt it to your architecture requirement. When the listener callsBack on the I/O within the get.exec(), you just need and interface that allows you to callback on the activity/fragment that has the UI and the adapter for your list so that it can be notified of the change in count-bytes-read-on-http-GET. The i/o callback will have to either reference the correct list entry in the adapter or provide some other ID so that it can be tied back to a particular GET. I've used handlers and the args in obtainMessageHandler() for that purpose. When the Handler provide and ID to a specific GET... then the listener will have a reference to the same ID or arg when it makes its callback to count i/o bytes.

for items 3 and 4, there is alot of stuff out there. Native Apache httpclients have pools/queues. Android volley offers that as well. More details here on the mechanics of handlers and 'obtainMessage' for the callbacks.