目录
在日常APP的开发中,通常情况下无可避免的要与调用网络后台数据接口。关于Android 网络请求接口的方式可以点击此此处进行学习。当我们实现一个从网络下载文件的功能时候,一般设计思路是这样的:使用Http发起请求,在IntentService的线程进行中下载,再配合Handler更新UI显示保持和用户交互。那么看了本篇就不需要那么麻烦了,因为Google因为帮我们封装好了一个方便下载的API叫做Downloadmanager。下面会先详细介绍实际使用方式,最后将这个API的实现原理。
本案例下载地址:
https://download.csdn.net/download/csdn_aiyang/10906816
DownloadManager是android2.3(API 9)以后系统提供下载的方法,是处理长期运行的HTTP下载的系统服务。客户端可以请求的URI被下载到一个特定的目标文件。客户端将会在后台与http交互进行下载,或者在下载失败,或者连接改变,重新启动系统后重新下载。还可以进入系统的下载管理界面查看进度。DownloadManger有两个内部类:Request 和Query。Request类可设置下载的一些属性;Query类可查询当前下载的进度等信息。三个公共方法:enqueue、query和remove。enqueue在队列中插入一个新的下载。当连接正常,并且DownloadManager准备执行这个请求时,开始自动下载。返回结果是系统提供的唯一下载ID,这个ID可以用于与这个下载相关的回调。query公共方法,用于查询下载信息。remove公共方法,用于删除下载,如果下载中则取消下载。同时会删除下载文件和记录。
<uses-permission android:name="android.permission.INTERNET" />;
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>;
创建对象,设置下载地址:
DownloadManager downloadManager = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
long id = downloadManager.enqueue(request);//每下载的一个文件对应一个id,通过此id可以查询数据
该方法返回成功取消的下载的个数。如果一个下载被取消了,所有相关联的、部分下载的和完全下载的文件都会被删除。
downloadManager.remove(id);
int cancers = downloadManager.remove(id_1, id_2, id_3);
小案例实现代码:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TextView down;
private TextView progress;
private TextView file_name;
private ProgressBar pb_update;
private DownloadManager downloadManager;
private DownloadManager.Request request;
public static String downloadUrl = "http://ucdl.25pp.com/fs08/2017/01/20/2/2_87a290b5f041a8b512f0bc51595f839a.apk";
Timer timer;
long id;
TimerTask task;
Handler handler =new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Bundle bundle = msg.getData();
int pro = bundle.getInt("pro");
String name = bundle.getString("name");
pb_update.setProgress(pro);
progress.setText(String.valueOf(pro)+"%");
file_name.setText(name);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
down = (TextView) findViewById(R.id.down);
progress = (TextView) findViewById(R.id.progress);
file_name = (TextView) findViewById(R.id.file_name);
pb_update = (ProgressBar) findViewById(R.id.pb_update);
down.setOnClickListener(this);
//创建对象
downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
request = new DownloadManager.Request(Uri.parse(downloadUrl));
request.setTitle("大象投教");
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setAllowedOverRoaming(false);
request.setMimeType("application/vnd.android.package-archive");
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
//创建目录
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
//设置文件存放路径
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS , "app-release.apk" ) ;
//手写更新UI
pb_update.setMax(100);
final DownloadManager.Query query = new DownloadManager.Query();
timer = new Timer();
task = new TimerTask() {
@Override
public void run() {
Cursor cursor = downloadManager.query(query.setFilterById(id));
if (cursor != null && cursor.moveToFirst()) {
if (cursor.getInt(
cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL) {
pb_update.setProgress(100);
install(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/app-release.apk" );
task.cancel();
}
String title = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE));
String address = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
int bytes_downloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
int bytes_total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
int pro = (bytes_downloaded * 100) / bytes_total;
Message msg =Message.obtain();
Bundle bundle = new Bundle();
bundle.putInt("pro",pro);
bundle.putString("name",title);
msg.setData(bundle);
handler.sendMessage(msg);
}
cursor.close();
}
};
timer.schedule(task, 0,1000);
// downloadManager.remove(id);
}
@Override
public void onClick(View v) {
id = downloadManager.enqueue(request);
task.run();
down.setClickable(false);
}
private void install(String path) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + path), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//4.0以上系统弹出安装成功打开界面
startActivity(intent);
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:gravity="center"
android:layout_height="match_parent">
<TextView
android:id="@+id/file_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="dxtj.apk"/>
<ProgressBar
android:id="@+id/pb_update"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/progressbar_color"
android:layout_marginBottom="20dp"
/>
<TextView
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginBottom="20dp"
android:text="0%"/>
<TextView
android:id="@+id/down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:background="@color/colorAccent"
android:text="立即下载"/>
</LinearLayout>
Progress_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@android:id/background">
<shape>
<corners android:radius="10dip" />
<gradient
android:angle="0"
android:centerColor="#e4e4e4"
android:centerY="0.75"
android:endColor="#e4e4e4"
android:startColor="#e4e4e4" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="10dip" />
<gradient
android:angle="0"
android:centerColor="#e4e4e4"
android:centerY="0.75"
android:endColor="#e4e4e4"
android:startColor="#e4e4e4" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="10dip" />
<gradient
android:angle="0"
android:endColor="@color/colorAccent"
android:startColor="@color/colorAccent" />
</shape>
</clip>
</item>
</layer-list>
public static class Request {
public static final int NETWORK_MOBILE = 1;
public static final int NETWORK_WIFI = 2;
public static final int VISIBILITY_HIDDEN = 2;
public static final int VISIBILITY_VISIBLE = 0;
public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
public static final int VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION = 3;
}
/**
* 方法1:
* 目录: Android -> data -> com.app -> files -> Download -> dxtj.apk
* 这个文件是你的应用所专用的,软件卸载后,下载的文件将随着卸载全部被删除
*/
request.setDestinationInExternalFilesDir( this , Environment.DIRECTORY_DOWNLOADS , "dxtj.apk" );
/**
* 方法2:
* 下载的文件存放地址 SD卡 download文件夹,dxtj.apk
* 软件卸载后,下载的文件会保留
*/
//在SD卡上创建一个文件夹
request.setDestinationInExternalPublicDir( "/epmyg/" , "dxtj.apk" ) ;
/**
* 方法3:
* 如果下载的文件希望被其他的应用共享
* 特别是那些你下载下来希望被Media Scanner扫描到的文件(比如音乐文件)
*/
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_MUSIC, "告白气球.mp3" );
/**
* 方法4
* 文件将存放在外部存储的确实download文件内,如果无此文件夹,创建之,如果有,下面将返回false。
* 系统有个下载文件夹,比如小米手机系统下载文件夹 SD卡--> Download文件夹
*/
//创建目录
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
//设置文件存放路径
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS , "dxtj.apk" ) ;
2、指定下载的网络类型:
//指定在WIFI状态下,执行下载操作。
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
//指定在MOBILE状态下,执行下载操作
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE);
//是否允许漫游状态下,执行下载操作
request.setAllowedOverRoaming(boolean);
//是否允许“计量式的网络连接”执行下载操作
request.setAllowedOverMetered(boolean); //默认是允许的。
3、定制Notification通知样式:
//设置Notification的标题和描述
request.setTitle("标题");
request.setDescription("描述");
//设置Notification的显示,和隐藏。
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
VISIBILITY_VISIBLE:通知显示,只是在下载任务执行的过程中显示,下载完成自动消失。(默认值)
VISIBILTY_HIDDEN: 通知将不会显示,如果设置该属性的话,必须要添加权限
Android.permission.DOWNLOAD_WITHOUT_NOTIFICATION.
VISIBILITY_VISIBLE_NOTIFY_COMPLETED : 通知显示,下载进行时,和完成之后都会显示。
VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION :下载完成时显示通知。
request.setMimeType("application/vnd.android.package-archive");
这是安卓.apk文件的类型。有些机型必须设置此方法,才能在下载完成后,点击通知栏的Notification时,才能正确的打开安装界面。不然会弹出一个Toast(can not open file)。其他文件类型的MimeType ,根据需求上网查一下吧 。如果设置了mimeType为application/cn.trinea.download.file,我们可以同时设置某个Activity的intent-filter为application/cn.trinea.download.file,用于响应点击的打开文件。
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<data android:mimeType="application/cn.trinea.download.file" />
</intent-filter>
</activity>
5、添加请求下载的网络链接的http头,比如User-Agent,gzip压缩等:
request.addRequestHeader(String header, String value);
Request类中封装了Notification ,简单设置属性就显示进度信息了。有时候需要在App中获取实时下载进度。而Query类就是提供查询的一些方法。 由于downloadManager将数据保存在数据库的,所以需要获得一个Cursor 结果集,通过结果集获得我们想要的数据。
DownloadManager.Query query = new DownloadManager.Query();
Cursor cursor = downloadManager.query(query.setFilterById(id));
if (cursor != null && cursor.moveToFirst()) {
//下载的文件到本地的目录
String address =
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
//已经下载的字节数
int bytes_downloaded =
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
//总需下载的字节数
int bytes_total =
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
//Notification 标题
String title =
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE));
//描述
String description =
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION));
//下载对应id
long id =
cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
//下载文件名称
String filename =
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
//下载文件的URL链接
String url =
cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI));
}
这只能获取一次,数据库中的信息。我们可以使用Timer类,每隔一段时间去查询数据库即可。也可以使用ContentProvider去访问:
private static final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads");
private DownloadContentObserver observer = new DownloadContentObserver();
@Override
protected void onResume() {
super.onResume();
getContentResolver().registerContentObserver(CONTENT_URI, true, observer);
}
@Override
protected void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(observer);
}
//ContentObserver 内部类监听下载进度
class DownloadContentObserver extends ContentObserver {
public DownloadContentObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
updateView();//更新UI
}
}
下载完成后,下载管理服务会发出DownloadManager.ACTION_DOWNLOAD_COMPLETE
这个广播,并传递downloadId作为参数。通过接受广播我们可以打开对下载完成的内容进行操作。
private CompleteReceiver completeReceiver;
class CompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// get complete download id
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
// to do here
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
completeReceiver = new CompleteReceiver();
//register download success broadcast
registerReceiver(completeReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(completeReceiver);
}
DownloadManager开始下载的入口enqueue方法,这个方法的源码如下:
public long enqueue(Request request) {
ContentValues values = request.toContentValues(mPackageName);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
long id = Long.parseLong(downloadUri.getLastPathSegment());
return id;
}
使用的ContentProvider方式,“request.toContentValues()”,将Request信息转换为ContentValues类。
然后调用ContentResolver进行插入“mResolver.insert()”,调用对应的ContentProvider的insert方法。传入的参数,URI是Downloads.Impl.CONTENT_URI,即"content://downloads/my_downloads",找到对应系统提供的DownloadProvider
。
DownloadProvider
类在系统源码的src/com/android/providers/downloads的路径下,找都其insert方法的实现,可以发现最后部分的代码:
public Uri insert(final Uri uri, final ContentValues values) {
...
// Always start service to handle notifications and/or scanning
final Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
即插入信息后,会启动DownloadService
开始进行下载。(源码学习|Android N DownloadManager源码分析)
DownloadService
的入口是onStartCommand方法,其中用mUpdateHandler发送消息MSG_UPDATE,mUpdateHandler处理消息的方式如下:
mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
private Handler.Callback mUpdateCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
...
final boolean isActive;
synchronized (mDownloads) {
isActive = updateLocked();
}
...
}
};
private boolean updateLocked() {
...
// Kick off download task if ready
final boolean activeDownload = info.startDownloadIfReady(mExecutor);
...
}
public boolean startDownloadIfReady(ExecutorService executor) {
synchronized (this) {
final boolean isReady = isReadyToDownload();
final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone();
if (isReady && !isActive) {
if (mStatus != Impl.STATUS_RUNNING) {
mStatus = Impl.STATUS_RUNNING;
ContentValues values = new ContentValues();
values.put(Impl.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
}
//启动DownloadThread开始下载任务
mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
mSubmittedTask = executor.submit(mTask);
}
return isReady;
}
}
从上面源码可以看,DownloadService
的onStartCommand方法,最终启动DownloadThread
,开始下载的任务(网络请求接口使用的是HttpURLConnection)。DownloadThread
在下载过程中,会更新DownloadProvider。
综上所述,DownloadManager的enqueue方法的流程是:
DownloadProvider插入信息 >> 启动DownloadService >> 开始DownloadThread进行下载
1、我发现,在下载的时候,发送Notification时 是没有声音的。也没有设置声音的方法。不过这影响不大。主要的功能实现就好。
2、因为这是系统的类,每个系统的Notification界面是不一样的。这就是每个rom厂家的自定义了。小米和魅族的就大不一样。魅族Notification上有一个下载暂停的按钮,而小米没有。所以导致Notification是不能统一的。其实,暂停的话用户可以点击notification,进入到下载管理界面,就有暂停按钮了。
3、会出现被用户手动禁用了下载器出现崩溃情况。需要做好版本兼容和弹框让用户手动开启。
参考链接:https://www.jianshu.com/p/e0496200769c DownloadManager的使用和解析-简书