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:
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:
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.