24

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.

Community
  • 1
  • 1
ductran
  • 10,043
  • 19
  • 82
  • 165
  • I did it in different way. `New thread` and call a static Handler which receives info from the `New thread` and sends it in the message queue to the activity. You could simply do this by calling a static method in the activity and see whether the activity is there or not in order to update info on the list. – Fahad Ishaque Nov 26 '13 at 13:57
  • Can you share the source code? Thanks!!! – Alston Aug 27 '14 at 13:20
  • @R4j What's the application name? Where I can I download it? Thanks! – Alston Aug 28 '14 at 03:15
  • 1
    @Stallman I can share only some snippet code. You can find here http://pastebin.com/MaT9TTpz and here http://pastebin.com/vvrsSibB and try your own. If you still have problems, just create new question on stackoverflow – ductran Aug 28 '14 at 03:47
  • Ok, Thanks, I'll create a new question... – Alston Aug 28 '14 at 05:22

2 Answers2

62

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
M-Wajeeh
  • 17,204
  • 10
  • 66
  • 103
  • 1
    Good point for `don't call notifyDatasetChanged() too frequently` because I got this problem. This way is similar to my try, but I have another problems: user can add more tasks to this current download list (check if they're not exist then add them to queue) **and** keep complete tasks in list even user exit app (do I need use database for caching?) is there any suggestion? – ductran Nov 27 '13 at 16:10
  • I wouldn't go for database, because all you need is a list of urls that are done or not. I will go with shared preferences. Even if structure is bit complex e.g. you want to store progress, path to actual file, how many retries done etc I will still store a list in preferences in form of json using Gson library. It will really be just a matter of one or more lines of code to read or write current progress to preferences using Gson in form of json. BTW thats my approach but you can go for Database if you want to. – M-Wajeeh Nov 27 '13 at 16:59
  • OK, I got it. I will update your code to adapt my requirement and then decide which ones to use. Thank you very much :) – ductran Nov 27 '13 at 17:12
  • 1
    Can anybody add AndroidManifest.xml also. Both receiver and service has to be added right ? – Ramesh_D Jul 03 '14 at 16:42
  • As I leave the download page and return, will it still show the download progress? Thanks – Alston Aug 26 '14 at 03:35
  • @M-WaJeEh Ok, I'm still trying. So far, I still don't understand how to merge the code with mine. Thanks again. – Alston Aug 26 '14 at 07:32
  • Could you please share the whole source code by delivering a hyperlink? Thanks!! – Alston Aug 27 '14 at 13:30
  • Why do you new 28 `progress()` in the ArrayAdapter? Does the number of the `progress` equal to the number of the item(the `things to be displayed` in the `listview`)? In my code, I use an `ArrayList` to pass the `items`, and the adapter use `getCount()` to populate the whole listview, is the `ArrayList` size the same as the progress number? THANKS FOR BILLION! – Alston Aug 28 '14 at 13:56
  • How to operate download task without `HttpURLConnection`? It really confused me. Thanks! – Alston Sep 01 '14 at 05:25
  • @Stallman its a sample code, you need to do you own work/download in `DownloadTask`. Right now I just pretend to do work by sleeping `Thread.sleep(mRand.nextInt(500));`. If you focus on basic things first and learn them instead of directly jumping into this code then it will be lot easier for you. – M-Wajeeh Sep 01 '14 at 05:54
  • @M-WaJeEh If we need to `// wait for finish` till all the download tasks are all completed, then call `publishCurrentProgressOneShot(true);` Since the `downloadtasks` are all completed, the progress must be 100 percent. I think we shouldn't wait any other task to be completed and then update the progress. – Alston Sep 04 '14 at 03:43
  • Hello all, I found the article interesting. If I wanted to start a download, and then another, and not all at once as I do? By using this source? Thanks in advance @ M-Wajeeh – Lorenzo Sogliani Nov 10 '14 at 10:16
  • how would you cancel a specific download I am not able to figure out this – ingsaurabh Nov 15 '14 at 10:33
  • @ M-WaJeEh Thanks I figured it out another way, BTW great sample – ingsaurabh Nov 21 '14 at 00:49
  • @M-WaJeEh how can we download multiple file same time like in listview every download click put 5 files for download like total list is 10 than 50 items download but it is all one by one download total 10 item download same time when one complete second is start automatically – Mahesh Jan 17 '15 at 11:37
  • @M-WaJeEh Thank you very much sir, whats equivalent `getView` on `RecyclerView`? could you update your post by using that? – DolDurma Jul 08 '16 at 20:27
  • Where is the file getting downloaded to? – suku Sep 01 '16 at 14:34
  • can we pause and resume the download? – Tara May 11 '18 at 07:44
2

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.

Community
  • 1
  • 1
Robert Rowntree
  • 6,230
  • 2
  • 24
  • 43
  • I think it didn't need the observer pattern. I wrapped the progress status into model and put it in adapter list view, then when progress change, I just need to notify dataset change and it can update list view. The download task will be in service and send progress update to fragment/ activity by BroadCastReceiver. Currently, I just consider which pool or queue task should I used. And does I need the database to cache complete download task? – ductran Nov 25 '13 at 15:39