作为android初学者,最近把疯狂android讲义和疯狂Java讲义看了一遍,看到书中介绍的知识点非常多,很难全部记住,为了更好的掌握基础知识点,我将开发一个网络音乐播放器-EasyMusic来巩固下,也当作是练练手。感兴趣的朋友可以看看,有设计不足的地方也欢迎指出。
开发之前首先介绍下该音乐播放器将要开发的功能(需求):
1.本地音乐的加载和播放;
2.网络音乐的搜索,试听和下载;
3.音乐的断点下载;
4.点击播放图标加载专辑图片,点击歌词加载歌词并滚动显示(支持滑动歌词改变音乐播放进度);
5.支持基于popupWindow的弹出式菜单;
6.支持后台任务栏显示和控制。
该篇主要是介绍实现多线程下载 :
首先介绍下多线程下载任务的建立过程
1.在netMusicFragment界面点击搜索到的网络音乐,弹出确认下载提示框,点击确认开始建立下载任务:
netMusicListView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
new AlertDialog.Builder(mContext)
.setTitle("下载提示")
.setMessage("是否下载该歌曲?")
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
}
})
//点击搜索到的网络音乐列表的第position项即可建立下载任务下载对应歌曲
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
createDownloadTask(position);
}
}).show();
}
});
//创建下载任务,网络音乐列表netMusicList中有对应position位置处的音乐信息
protected void createDownloadTask(int position) {
String url = (String)netMusicList.get(position).get("audio");
String title = (String)netMusicList.get(position).get("title");
String artist = (String)netMusicList.get(position).get("artist");
String targetFile = MainActivity.downloadedPath + "/" + title + ".mp3";
//这里的下载线程暂时设置为固定的数值3,有兴趣的可以改成手动选择线程数量
DownloadUtil downloadUtil = new DownloadUtil(url, targetFile, 3, mContext);
Log.d(TAG, "DownloadUrl = " + url);
Map<String,Object> map = new HashMap<String, Object>();
map.put("title", title);
map.put("artist", artist);
map.put("url", url);
map.put("downloadUtil", downloadUtil);
//主activity的executeDownloadUtil方法里调用service执行下载任务
((MainActivity)this.getActivity()).executeDownloadUtil(downloadUtil);
DownloadFragment.downloadingMusic.add(map);
}
2.通过MainActivity来调用服务执行后台下载。
public void executeDownloadUtil(DownloadUtil util) {
Toast.makeText(mContext, "executeDownloadUtil", Toast.LENGTH_LONG)
.show();
musicService.downloadMusic(util);
}
musicService执行下载方法downloadMusic(util),将下载任务放进线程池中,当线程池中有空闲的线程时会开始下载。之所以将下载任务的执行放在service中是因为service可以后台运行,而如果在activity中执行下载任务的话,后台运行播放器的时候下载任务会暂停。musicService的下载相关代码如下:
public void downloadMusic(final DownloadUtil util) {
//传进来的下载任务将放入线程池
pool.execute(new DownloadRunnable(util));
}
private class DownloadRunnable implements Runnable {
private DownloadUtil util;
public DownloadRunnable(DownloadUtil util) {
this.util = util;
}
@Override
public void run() {
util.download();
}
}
多线程下载工具的代码如下,相关注释已经写的很清楚了。
package com.sprd.easymusic.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
public class DownloadUtil {
private String TAG = "DownloadUtil";
private String targetUrl = null;
private String targetFile = null;
//多线程下载的线程数量
private int threadNum;
private Context mContext;
//下载文件的大小
private int fileSize;
//下载任务是否是暂停状态
private boolean pause;
//下载任务是否被删除了
private boolean delete;
//下载完成的线程数,当所有线程下载完成时该值等于threadNum
private int downloadSuccessThread;
//下载的线程数组
private DownloadThread[] downloadThreads;
public DownloadUtil (String targetUrl, String targetFile, int threadNum, Context context) {
this.targetUrl = targetUrl;
this.targetFile = targetFile;
this.threadNum = threadNum;
downloadThreads = new DownloadThread[threadNum];
mContext = context;
}
public void download() {
try {
URL url = new URL(targetUrl);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5000);
//获取下载文件的大小
fileSize = conn.getContentLength();
conn.disconnect();
Log.d(TAG, "download:fileSize = " + fileSize);
File file = new File(targetFile);
if (!file.exists()) {
file.createNewFile();
Log.d(TAG, "create file:" + file);
}
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
raf.setLength(fileSize);
raf.close();
//划分每个线程的下载长度
int currentFileSize = fileSize / threadNum == 0 ? fileSize / threadNum : fileSize / threadNum + 1;
for (int i=0; i<threadNum; i++) {
downloadThreads[i] = new DownloadThread(targetUrl, targetFile, currentFileSize * i, currentFileSize);
downloadThreads[i].start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
//获取线程进度
public int getDownloadProgress() {
int downloadLength = 0;
for (DownloadThread thread : downloadThreads) {
if (thread != null) downloadLength += thread.length;
}
Log.d(TAG, "fileSize = " + fileSize + " downloadLength = " +downloadLength);
return fileSize > 0 ? downloadLength * 100 / fileSize: 0;
}
public boolean isPause() {
return pause;
}
public void setPause(boolean pause) {
this.pause = pause;
}
public boolean isDelete() {
return delete;
}
public void setDelete(boolean delete) {
this.delete = delete;
}
//下载线程
private class DownloadThread extends Thread {
//当前线程下载的长度
public int length;
//下载的资源链接
private String downloadUrl = null;
//下载内容到file文件中
private String file;
//下载的起始位置
private int startPos;
//需要下载的长度
private int currentFileSize;
//读取网络音乐数据的缓存
private BufferedInputStream bis;
public DownloadThread (String downloadUrl, String file, int startPos, int currentFileSize) {
this.downloadUrl = downloadUrl;
this.file = file;
this.startPos = startPos;
this.currentFileSize = currentFileSize;
}
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection)new URL(downloadUrl).openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
InputStream is = conn.getInputStream();
//获取的输入流数据跳过指定字节数
is.skip(startPos);
File downloadFile = new File(file);
RandomAccessFile raf = new RandomAccessFile(downloadFile, "rwd");
//对应文件写入位置也跳过指定字节数
raf.seek(startPos);
bis = new BufferedInputStream(is);
int hasRead = 0;
byte[] buff = new byte[1024*4];
while (length < currentFileSize && (hasRead = bis.read(buff)) > 0 && !delete) {
while (pause){
Log.d(TAG, "DownloadUtil pause!");
}
if (!pause) {
//字节读入对应文件
raf.write(buff, 0, hasRead);
length += hasRead;
Log.d(TAG, "read " + hasRead + " bytes");
}
}
Log.d(TAG, "download success? " + (fileSize == length));
//关闭资源和链接
raf.close();
bis.close();
if (delete) {
boolean isDeleted = downloadFile.delete();
Log.d(TAG, "delete file success ? " + isDeleted);
} else {
Log.d(TAG, "run:currentFileSize = " + currentFileSize + " downloadLength = " + length);
//当前线程下载完成时,DownloadUtil的downloadSuccessThread值+1
downloadSuccessThread++;
if (downloadSuccessThread == threadNum) {
Log.d(TAG, "download success");
Intent notifyIntent = new Intent("action_download_success");
notifyIntent.putExtra("url", targetUrl);
//给DownloadFragment发送下载完成的广播
mContext.sendBroadcast(notifyIntent);
scanFileToMedia(targetFile);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//将新下载的歌曲扫描到媒体库中,为后面MusicListFragment添加下拉刷新做铺垫
public void scanFileToMedia(final String url) {
new Thread(new Runnable() {
public void run() {
MediaScannerConnection.scanFile(mContext, new String[] {url}, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
Log.d(TAG, "scan completed : file = " + url);
}
});
}
}).start();
}
}
其中实现多线程下载的关键是多个线程的任务分割(每个线程下载的起始位置以及需要下载的长度-最后一个线程下载长度需要额外注意,因为其下载长度可能与前面的线程不同)
最后不要忘了在AndroidManifest中添加权限(读写SD卡,创建和删除文件,连接网络):
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.INTERNET"/>
音乐播放器已完成,下载地址:
Android音乐播放器