Android實現多執行緒下載並顯示通知
1、前言
相信大家都使用過一些可以下載檔案的 App,在下載列表中通常都會顯示一個進度條實時地更新下載進度,現在的下載都是斷點重傳的,也就是在暫停後,重新下載會依照之前進度接著下載。
我們這個下載的案例是有多個執行緒同時下載一個任務,並能提供多個檔案同時下載,在下載的同時會顯示通知,因為下載執行緒是放在 Service 中的,所以就算程式執行在後臺也可以繼續下載。
當啟動下載時,就會發送通知提示開始下載,下載完成後在列表和通知欄中都會移除這個任務。有下載和停止兩個 Button 控制下載。
2、軟體結構
我們這個下載的案例也算一個小型的軟體,結構具有一定程度的複雜性,在展示程式碼前,我先來分析一下這個軟體的結構。
這就是整個工程的分類,我們將控制下載的程序資訊以資料庫的形式記錄下來,這樣可以避免重複下載,而且就算程式在後臺被殺死,重新開啟後也可以繼續下載。
我們之前演示 APP 時,大家已經看到了佈局,就是一個很簡單的 RecyclerView,因為這個程式的重點是在多執行緒等後臺操作上,所以在 UI 設計就隨便了些,現在一般的 APP 都已經用一個 Button 來實現開始和暫停,有興趣的朋友也可以自己用 selector 來設計一下,這裡就不做了。
這些類之間的關係大致如上圖,FileInfo 和 ThreadInfo 是兩個實體類。FileInfo 是記錄要下載的檔案的資訊,之前說過我們是多執行緒下載,所以 ThreadInfo 對應的就是一個 FileInfo 物件所需要的下載時的執行緒資訊。
一個 FileInfo 對應一個 DownloadTask 下載任務,下載肯定是放在後臺的,所以我們要使用 Service。用 DownloadService 來啟動每個 DownloadTask。DownloadTask 中就處理執行緒去進行下載和傳遞訊息更新 UI 並將資料存入資料庫,DownloadThread 類放在 DownloadTask 裡,每一個 DownloadThread 處理一個 ThreadInfo 對應的資訊。資料庫中儲存的就是 ThreadInfo 資訊。
3、實體類
FileInfo.java:
public class FileInfo implements Serializable {
private int id;
private int length;
private String url;
private String name;
private int finished;
public FileInfo() {
}
public FileInfo(int id, int length, String url, String name, int finished) {
this.id = id;
this.length = length;
this.url = url;
this.name = name;
this.finished = finished;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setFinished(int finished) {
this.finished = finished;
}
public int getFinished() {
return finished;
}
}
FileInfo 的屬性一目瞭然,檔案 id,檔案長度,下載地址,檔名,下載進度。因為我們要把這個物件在 Activity 和 Service 之間傳遞,所以我們要讓它實現序列化,用 Serializable 操作比 Parcelable 簡單。
ThreadInfo.java:
public class ThreadInfo {
private int id;
private String url;
private int start;
private int end;
private int finished;
public ThreadInfo() {
}
public ThreadInfo(int id, String url, int start, int end, int finished) {
this.id = id;
this.url = url;
this.start = start;
this.end = end;
this.finished = finished;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public int getFinished() {
return finished;
}
public void setFinished(int finished) {
this.finished = finished;
}
}
ThreadInfo 的屬性與 FileInfo 大同小異,其中標識這個執行緒是屬於哪個任務的是 url,因為 url 是唯一的,後面也要根據 url 確定是哪個 FileInfo。
start 和 end 是兩個關鍵的屬性,代表這個執行緒要完成從 start 開始到 end 這一區間的下載,然後配合 finished 就能知道這個執行緒下載到哪裡啦。像一個 100 KB 的檔案,我們用三個執行緒對它進行下載,三個執行緒分別完成 0KB - 33KB,33KB - 66KB,66KB - 100KB。
4、資料庫
我建立了一個常數類用來儲存一些經常用到的常數,Constant.java:
public class Constant {
public static final String DATABASE_NAME = "info.db"; //資料庫名稱
public static final int DATABASE_VERSION = 1; //資料庫版本
public static final String TABLE_NAME = "threadInfo"; //表名
public static final String _ID = "_id";
public static final String THREAD_ID = "thread_id";
public static final String URL = "url";
public static final String START = "start";
public static final String END = "end";
public static final String FINISHED = "finished";
public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory()
+ File.separator + "download";
public static final String ACTION_START = "ACTION_START";
public static final String ACTION_STOP = "ACTION_STOP";
public static final int MSG_INIT = 0x1;
public static final int MSG_BIND = 0x2;
public static final int MSG_START = 0x3;
public static final int MSG_UPDATE = 0x4;
public static final int MSG_FINISH = 0x5;
}
前面都是資料庫相關的定義,相信有一定的朋友都能理解。後面的 Action 和 MSG 是我們用來開啟服務和進行 Activity 和 Service 通訊的標識。這個程式用的是 Handler 來傳遞資料,這個後面再介紹。
DBHelper.java:
public class DBHelper extends SQLiteOpenHelper {
private volatile static DBHelper helper;
public static DBHelper getInstance(Context context) {
if (helper == null) {
synchronized (DBHelper.class) {
if (helper == null) {
helper = new DBHelper(context);
}
}
}
return helper;
}
private DBHelper(Context context) {
super(context, Constant.DATABASE_NAME, null, Constant.DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
String sql = "create table " + Constant.TABLE_NAME + " (" +
Constant._ID + " Integer primary key autoincrement, " +
Constant.THREAD_ID + " Integer," +
Constant.URL + " text," +
Constant.START + " Integer," +
Constant.END + " Integer," +
Constant.FINISHED + " Integer)";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
我們的資料庫中只有一張表,所以並不複雜,這些基礎的使用我在我的部落格Android上SQLite的基本應用(一)中有介紹。
因為對資料庫的使用不能有太多入口,所以我們對資料庫的訪問需要使用單例模式,這裡用雙重校驗鎖。
DBManager.java:
public class DBManager {
private DBHelper helper;
public DBManager(Context context) {
helper = DBHelper.getInstance(context);
}
public synchronized void insertThread(ThreadInfo threadInfo) {
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(Constant.THREAD_ID, threadInfo.getId());
values.put(Constant.URL, threadInfo.getUrl());
values.put(Constant.START, threadInfo.getStart());
values.put(Constant.END, threadInfo.getEnd());
values.put(Constant.FINISHED, threadInfo.getFinished());
db.insert(Constant.TABLE_NAME, null, values);
db.close();
}
public synchronized void updateData(String url, int thread_id, int finished) {
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(Constant.FINISHED, finished);
db.update(Constant.TABLE_NAME, values, Constant.URL + "=? and " +
Constant.THREAD_ID + "=?", new String[]{url, thread_id + ""});
db.close();
}
public synchronized void deleteThread(String url) {
SQLiteDatabase db = helper.getWritableDatabase();
db.delete(Constant.TABLE_NAME, Constant.URL + "=?",
new String[]{url});
db.close();
}
public List<ThreadInfo> getThreads(String url) {
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = db.query(Constant.TABLE_NAME, null, Constant.URL + "=?",
new String[]{url}, null, null, Constant._ID + " asc");
List<ThreadInfo> list = cursorTolist(cursor);
cursor.close();
db.close();
return list;
}
public static List<ThreadInfo> cursorTolist(Cursor cursor) {
List<ThreadInfo> list = new ArrayList<ThreadInfo>();
while (cursor.moveToNext()) {
int _id = cursor.getInt(cursor.getColumnIndex(Constant.THREAD_ID));
String url = cursor.getString(cursor.getColumnIndex(Constant.URL));
int start = cursor.getInt(cursor.getColumnIndex(Constant.START));
int end = cursor.getInt(cursor.getColumnIndex(Constant.END));
int finished = cursor.getInt(cursor.getColumnIndex(Constant.FINISHED));
ThreadInfo threadInfo = new ThreadInfo(_id, url, start, end, finished);
list.add(threadInfo);
}
return list;
}
}
我們通常在開發中都是把資料庫定義和資料庫功能實現分開處理,這樣使得結構更加清晰,也方便管理。
例項化 DBManager 的同時也獲得了 DBHelper 物件,資料庫幫助類就是一個開啟資料庫的鑰匙,有了它就能進行插入、更新、刪除、查詢等資料操作。
因為插入、更新、刪除都是對資料的修改,而我們是多執行緒,可能同時有幾個執行緒對資料進行修改,所以我們要給這個方法加上同步。
5、Activity與Service通訊
前面介紹常數類,說過我們的資料傳輸是使用 Handler,其實也可以使用廣播。雖然使用起來很簡單,只要註冊了在任何地方都能使用,但因為廣播是系統元件,所以效率肯定不及 Handler,它是專門用來做執行緒通訊的,對於我們這個下載案例使用它是很合適的。
要使用 Handler 進行跨元件通訊,我們需要使用到 Messenger,也就是信使。比如要從 Service 向 Activity 傳送訊息,我們要先獲得帶有 Activity 的 Handler 的信使,然後就可以通過這個信使從 Service 傳送訊息到 Activity 的 Handler 中了。
而怎麼獲得 Messenger,這就要用到 Service 的綁定了,我們可以在 onBind() 方法中返回 Service 的 Messenger,那麼在 Activity 繫結服務的時候就可以獲得這個信使,於是實現了 Activity 到 Service 的單向通訊。通過這個 Messenger 就可以向 Service 傳送訊息,訊息裡包含 Activity 的 Messenger,這樣 Serivce 也獲得了 Activity 的信使,就實現了兩者的雙向通訊。
DownloadService.java:
public class DownloadService extends Service {
private Map<Integer, DownloadTask> mTasks =
new LinkedHashMap<Integer, DownloadTask>();
private Messenger mActivityMessenger;
@SuppressLint("HandlerLeak")
private Handler mHandle = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Constant.MSG_INIT :
FileInfo fileInfo = (FileInfo) msg.obj;
DownloadTask task = new DownloadTask(DownloadService.this, mActivityMessenger, fileInfo, 3);
task.download();
mTasks.put(fileInfo.getId(), task);
Message startMsg = new Message();
startMsg.what = Constant.MSG_START;
startMsg.obj = fileInfo;
try {
mActivityMessenger.send(startMsg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
case Constant.MSG_BIND :
mActivityMessenger = msg.replyTo;
break;
}
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (Constant.ACTION_START.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("FileInfo");
DownloadTask task = mTasks.get(fileInfo.getId());
if (task == null || task.isPause || task.end) {
new InitThread(fileInfo).httpConnection();
}
} else if (Constant.ACTION_STOP.equals(intent.getAction())) {
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("FileInfo");
DownloadTask task = mTasks.get(fileInfo.getId());
if (task != null) {
task.isPause = true;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Messenger messenger = new Messenger(mHandle);
return messenger.getBinder();
}
private class InitThread {
private FileInfo mFileInfo;
public InitThread(FileInfo fileInfo) {
this.mFileInfo = fileInfo;
}
public void httpConnection() {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(mFileInfo.getUrl())
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream is = null;
RandomAccessFile raf = null;
try {
is = response.body().byteStream();
int length = (int) response.body().contentLength();
File dir = new File(Constant.DOWNLOAD_PATH);
if (!dir.exists()) {
dir.mkdir();
}
File file = new File(dir, mFileInfo.getName());
raf = new RandomAccessFile(file, "rwd");
mFileInfo.setLength(length);
mHandle.obtainMessage(Constant.MSG_INIT, mFileInfo).sendToTarget();
} catch (IOException e) {
e.printStackTrace();
} finally {
raf.close();
is.close();
}
}
});
}
}
}
DownloadService 有兩個全域性變數,mTasks 是一個鍵對,用來確認每個下載任務的當前狀態,每次啟動一個新的任務,就會放入 mTasks 中。mActivityMessenger 是我們從 Activity 中獲得的信使,它的初始化是在 onCreate() 中繫結服務時發生,繫結的時候會發送標識為 MSG_BIND 的訊息給 Service,我們可以利用 Message 物件的 replyto 獲得信使。
mHandler 是處理由 Activity 傳來的訊息,MSG_INIT 代表點選下載按鈕時會觸發初始化,DownloadService 中有個 InitThread,我們要先獲得檔案的長度,如果不知道這個我們很難實時更新下載進度。我訪問網路是用的 OKHttp,對它不瞭解的朋友可以看我的部落格Android網路框架OKHttp初解。
在 onResponse() 中做的操作是在其它執行緒中,所以我們的 InitThread 不用繼承執行緒也可以讓網路請求執行在其它執行緒中。我們獲得了檔案長度後,就傳送訊息通知 Handler 初始化完成了,可以開啟 Task,進行下載。這時就要同步傳送訊息給 Activity 下載開始了。
onStartCommand() 是在使用 startService() 時呼叫,我用 Intent 來控制兩個 Button 的點選效果。Action 為 START 時,說明要開始下載。這時從 intent 中獲取 FileInfo 物件,然後在 mTasks 中找到對應的 DownloadTask,這時要避免多次點選,只有下載沒有開始,暫停下載和下載結束這幾種情況下進行初始化;當 Action 為 STOP 時,設定這個下載任務為暫停。
onBinder() 方法則是在 Service 與 Activity 繫結時呼叫的方法,我們可以把它看作 Service 與 Activity 的連線點,就是通過這個方法把 Service 的 Messenger 傳遞給 Activity。
MainActivity.java:
public class MainActivity extends AppCompatActivity {
private RecyclerView mRecycler;
private List<FileInfo> list;
private SimpleAdapter mAdapter;
private NotificationUtil mUtil;
private Messenger mServiceMessenger;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getPermission();
list = new ArrayList<FileInfo>();
FileInfo fileInfo = new FileInfo(0, 0, "http://m.down.sandai.net/MobileThunder/Android_5.29.2.4520/XLWXguanwang.apk",
"xunlei.apk", 0);
FileInfo fileInfo2 = new FileInfo(1, 0, "",
"view.jpg", 0);
FileInfo fileInfo3 = new FileInfo(2, 0, "http://gdown.baidu.com/data/wisegame/d28f2315db2b6f97/UCliulanqi_682.apk",
"UC.apk", 0);
list.add(fileInfo);
list.add(fileInfo2);
list.add(fileInfo3);
mRecycler = (RecyclerView) findViewById(R.id.recycler);
mAdapter = new SimpleAdapter(this, list);
mRecycler.setAdapter(mAdapter);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(this,
LinearLayoutManager.VERTICAL, false);
mRecycler.setLayoutManager(mLayoutManager);
mUtil = new NotificationUtil(this);
Intent intent = new Intent(this, DownloadService.class);
bindService(intent, sc, Service.BIND_AUTO_CREATE);
}
private ServiceConnection sc = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mServiceMessenger = new Messenger(service);
Messenger messenger = new Messenger(mHandler);
Message msg = new Message();
msg.what = Constant.MSG_BIND;
msg.replyTo = messenger;
try {
mServiceMessenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
public void getPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}
}
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Bundle bundle;
FileInfo fileInfo;
switch (msg.what) {
case Constant.MSG_START :
fileInfo = (FileInfo) msg.obj;
mUtil.showNotification(fileInfo);
break;
case Constant.MSG_UPDATE :
bundle = msg.getData();
int progress = (int) bundle.get("finished");
String url = (String) bundle.get("url");
int id = (int) bundle.get("id");
mAdapter.updateProgress(url, progress);
mUtil.updateNotification(id, progress);
break;
case Constant.MSG_FINISH :
fileInfo = (FileInfo) msg.obj;
mAdapter.removeDownload(fileInfo.getUrl());
Toast.makeText(MainActivity.this, fileInfo.getName() + "已下載完成!",
Toast.LENGTH_SHORT).show();
mUtil.cancelNotification(fileInfo.getId());
break;
}
super.handleMessage(msg);
}
};
@Override
protected void onDestroy() {
unbindService(sc);
super.onDestroy();
}
}
MainActivity 的作用有設定佈局,許可權,設定 Handler 繫結服務。在 Handler 處理各種訊息觸發的邏輯。
SimpleAdapter.java:
public class SimpleAdapter extends RecyclerView.Adapter<SimpleAdapter.MyViewHolder> {
private Context context;
private LayoutInflater mInflater;
private List<FileInfo> mDatas;
private List<Boolean> isFirsts;
public SimpleAdapter(Context context, List<FileInfo> datas) {
this.context = context;
this.mDatas = datas;
mInflater = LayoutInflater.from(context);
isFirsts = new ArrayList<Boolean>();
for (int i = 0; i < datas.size(); i++) {
isFirsts.add(true);
}
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.item_layout, parent, false);
MyViewHolder viewHolder = new MyViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final FileInfo fileInfo = mDatas.get(position);
if (isFirsts.get(position)) {
holder.tv.setText(fileInfo.getName());
holder.progress.setMax(100);
holder.start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(Constant.ACTION_START);
intent.putExtra("FileInfo", fileInfo);
context.startService(intent);
}
});
holder.stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(Constant.ACTION_STOP);
intent.putExtra("FileInfo", fileInfo);
context.startService(intent);
}
});
isFirsts.set(position, false);
}
holder.progress.setProgress(fileInfo.getFinished());
}
@Override
public int getItemCount() {
return mDatas.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tv;
Button start, stop;
ProgressBar progress;
public MyViewHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.text);
start = (Button) itemView.findViewById(R.id.start);
stop = (Button) itemView.findViewById(R.id.stop);
progress = (ProgressBar) itemView.findViewById(R.id.progress);
}
}
public void removeDownload(String url) {
for (int i = 0; i < mDatas.size(); i++) {
if (mDatas.get(i).getUrl().equals(url)) {
mDatas.remove(i);
isFirsts.remove(i);
for (int j = 0; j < isFirsts.size(); j++) {
isFirsts.set(j, true);
}
notifyItemChanged(i);
notifyDataSetChanged();
break;
}
}
}
public void updateProgress(String url, int progress) {
for (int i = 0; i < mDatas.size(); i++) {
if (mDatas.get(i).getUrl().equals(url)) {
FileInfo fileInfo = mDatas.get(i);
fileInfo.setFinished(progress);
notifyDataSetChanged();
break;
}
}
}
}
在介面卡中有幾個要注意的,控制元件的初始化比較簡單,兩個 Button 就是設定 Intent 傳遞檔案資訊啟動服務,前面已經提到了。這裡要看到的是 updateProgress() 和 removeDownload() 兩個方法,前面的演示大家可以看見當下載任務完成後就會把 item 從列表中移除,而移除後 Item 在集合中的 id 就會發生變化,所以我們要找到對應的要更新的 item 就要用可以唯一標識的屬性,這裡用的是檔案的 url。因為在列表中不會有相同的 url 的 item。
這裡有個集合 isFirsts,因為對控制元件的初始化的操作有很多隻用做一次,而 onBindViewHolder() 方法每次重新整理都會呼叫一次,只是不必要的,所以用這個集合來確定是否是第一次呼叫這個方法。在移除 Item 後,要重新設定 isFirsts,否則就不會繪製好要變化的列表。
NotificationUtil 是用來設定通知的,NotificationUtil.java:
public class NotificationUtil {
private NotificationManager manager;
private Map<Integer, Notification> notifications;
private Context mContext;
public NotificationUtil(Context context) {
this.mContext = context;
manager = (NotificationManager) context
.getSystemService(context.NOTIFICATION_SERVICE);
notifications = new HashMap<>();
}
public void showNotification(FileInfo fileInfo) {
if (!notifications.containsKey(fileInfo.getId())) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
.setSmallIcon(R.mipmap.ic_launcher)
.setTicker(fileInfo.getName() + "開始下載");
builder.setAutoCancel(true);
Intent intent = new Intent(mContext, MainActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(mContext, fileInfo.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pIntent);
RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),
R.layout.notification_layout);
//TextView
remoteViews.setTextViewText(R.id.text, fileInfo.getName());
//開始按鈕
Intent intentStart = new Intent(mContext, DownloadService.class);
intentStart.setAction(Constant.ACTION_START);
intentStart.putExtra("FileInfo", fileInfo);
PendingIntent pStart = PendingIntent.getService(mContext, fileInfo.getId(), intentStart,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.startN, pStart);
//停止按鈕
Intent intentStop = new Intent(mContext, DownloadService.class);
intentStop.setAction(Constant.ACTION_STOP);
intentStop.putExtra("FileInfo", fileInfo);
PendingIntent pStop = PendingIntent.getService(mContext, fileInfo.getId(), intentStop,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.stopN, pStop);
Notification notification = builder.setContent(remoteViews).build();
manager.notify(fileInfo.getId(), notification);
notifications.put(fileInfo.getId(), notification);
}
}
public void cancelNotification(int id) {
manager.cancel(id);
notifications.remove(id);
}
public void updateNotification(int id, int progress) {
Notification notification = notifications.get(id);
if (notification != null) {
notification.contentView.setProgressBar(R.id.progressN, 100, progress, false);
manager.notify(id, notification);
}
}
}
Notification 的設定很簡單,不過這裡要注意 PendingIntent 的 getActivity() 方法,裡面有一個引數 requestCode,這也是一個標識碼。如果我們給每個 PendingIntent 都設定了相同的 requestCode,就會導致當傳送多個 Notification 的時候,點選通知每次獲取的都是第一個通知對應的內容。所以如果要顯示多個佈局相同的通知,就要給每個通知設定不同的 requestCode。
回到 MainActivity,要繫結服務需要 ServiceConnection 物件,onServiceConnected() 方法中的 Service 就有我們在 onBinder() 返回的 Messenger,利用這個信使把 Activity 的 Messenger 傳送過去,就實現了雙向通訊了。
在 Handler 中共處理下載開始、更新和完成三種情況時的對 UI 的更新。
6、下載邏輯
DownloadTask.java:
public class DownloadTask {
private Context mContext;
private FileInfo mFileInfo;
private DBManager manager;
private long mFinished = 0;
public boolean isPause = false;
private int mThreadCount = 1;
private List<DownloadThread> mThreadList = null;
private Messenger mMessenger;
public boolean end = false;
public DownloadTask(Context context, Messenger messenger, FileInfo fileInfo, int threadCount) {
this.mContext = context;
this.mFileInfo = fileInfo;
this.mMessenger = messenger;
manager = new DBManager(mContext);
this.mThreadCount = threadCount;
}
public void download() {
List<ThreadInfo> threads = manager.getThreads(mFileInfo.getUrl());
if (threads.size() == 0) {
int length = mFileInfo.getLength() / mThreadCount;
for (int i = 0; i < mThreadCount; i++) {
ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(),
i * length, (i + 1) * length, 0);
if (i == mThreadCount - 1) {
threadInfo.setEnd(mFileInfo.getLength());
}
threads.add(threadInfo);
manager.insertThread(threadInfo);
}
}
mThreadList = new ArrayList<DownloadThread>();
for (ThreadInfo info: threads) {
DownloadThread thread = new DownloadThread(info);
mThreadList.add(thread);
thread.httpConnection();
}
}
private synchronized void checkAllThreadsFinished() {
Boolean allFinished = true;
for (DownloadThread thread : mThreadList) {
if (!thread.isFinished) {
allFinished = false;
break;
}
}
if (allFinished) {
Message msg = new Message();
msg.what = Constant.MSG_FINISH;
msg.obj = mFileInfo;
try {
mMessenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
manager.deleteThread(mFileInfo.getUrl());
end = true;
}
}
private class DownloadThread {
private ThreadInfo mThreadInfo;
public boolean isFinished = false;
public DownloadThread (ThreadInfo threadInfo) {
this.mThreadInfo = threadInfo;
}
public void httpConnection() {
OkHttpClient client = new OkHttpClient();
int start = mThreadInfo.getStart() + mThreadInfo.getFinished();
mFinished += mThreadInfo.getFinished();
Request request = new Request.Builder()
.url(mFileInfo.getUrl())
.header("RANGE", "bytes=" + start +
"-" + mThreadInfo.getEnd())
.build();
client.newCall(request).enqueue(new MyCallBack(mThreadInfo));
}
private class MyCallBack implements Callback {
private ThreadInfo mThreadInfo;
public MyCallBack(ThreadInfo threadInfo) {
this.mThreadInfo = threadInfo;
}
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
File dir = new File(Constant.DOWNLOAD_PATH);
int start = mThreadInfo.getStart() + mThreadInfo.getFinished();
if (!dir.exists()) {
dir.mkdir();
}
final File file = new File(dir, mFileInfo.getName());
InputStream is = null;
RandomAccessFile raf = null;
try {
is = response.body().byteStream();
byte[] buf = new byte[1024];
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
int len;
int progress = (int)(mFinished * 100 / mFileInfo.getLength());
while ((len = is.read(buf)) != -1) {
raf.write(buf, 0, len);
mFinished += len;
int nowProgress = (int)(mFinished * 100 / mFileInfo.getLength());
mThreadInfo.setFinished(mThreadInfo.getFinished() + len);
if (nowProgress - pro