版本升级对于app来说已是很是常见的功能了,每次项目的版本迭代、新功能的开发都须要下载更新新版本,经过安装新版原本实现咱们的迭代。固然除了这种方式,实际上也有热更新与热修复的存在,无需安装的状况下实现版本的迭代,并且不少大型的项目在有了大量用户的积累后也大都采起了灰度发布的功能,先小范围升级试用,在正式推向市场。今天我只想单纯来说讲基于系统自带的DownloadManager来实现的下载更新。android
画图不易,这张流程图几乎包含了app检查更新的全部涉及到的流程,像流程图中进度框、下载失败的弹框,MD5校验我的以为能够不须要,通常像DownloadManager来实现下载更新只须要在后台下载,下载完成用系统的Notification进行通知便可,而后自动弹出安装界面,这是个标准的流程。 git
file://xxx
格式的Uri来访问文件,须要使用FileProvider,Uri格式为content://xxx
。DownloadManager下载管理器是一种处理长时间运行的HTTP下载的系统服务。客户端能够请求将URI下载到特定目标文件。下载管理器将在后台进行下载,负责HTTP交互并在发生故障或跨链接更改和系统从新启动后重试下载。翻译过来的始终感受很差,如下是官方的原话 (官方传送门)github
The download manager is a system service that handles long-running HTTP downloads. Clients may request that a URI be downloaded to a particular destination file. The download manager will conduct the download in the background, taking care of HTTP interactions and retrying downloads after failures or across connectivity changes and system reboots.shell
Apps that request downloads through this API should register a broadcast receiver for
ACTION_NOTIFICATION_CLICKED
to appropriately handle when the user clicks on a running download in a notification or from the downloads UI.api
Note that the application must have the
Manifest.permission.INTERNET permission
to use this class.浏览器
从概念上都已经明确说明了DownloadManager系统下载服务的优越性:
1.能够长时间在后台运行下载
2.能够指定任意的下载路径,也能够支持Android Q
3.下载过程当中碰见问题或者更改网络会重试下载,断点续传
4.原生系统下载服务,不依赖第三方,兼容性和稳定性无疑最好
5.默认已经帮你封装好了系统栏通知、wifi/移动网络/漫游等等下载限制缓存
类/常量/方法 | 介绍 |
---|---|
DownloadManager.Query | 主要用来在下载的过程当中查询过滤,好比下载状态、进度等 |
DownloadManager.Request | 下载服务一些配置、下载地址、下载路径、通知栏配置、网络限制、媒体类型等 |
ACTION_DOWNLOAD_COMPLETE | 下载完成后,由下载管理器发送的广播意图操做 |
ACTION_NOTIFICATION_CLICKED | 当用户从系统通知或下载UI单击正在运行的下载时,下载管理器发送广播意图操做 |
ACTION_VIEW_DOWNLOADS | 启动活动以显示全部下载的意图操做,说白了手机系统的下载管理界面 |
COLUMN_BYTES_DOWNLOADED_SO_FAR | 目前下载的字节数,须要下载进度条的用获得 |
COLUMN_TOTAL_SIZE_BYTES | 下载文件的总大小,单位为字节,须要下载进度条的用获得 |
COLUMN_LOCAL_URI | 下载的文件将存储在Uri中,注意:N以前是file://xxx ,N以后是content://xxx |
EXTRA_DOWNLOAD_ID | 在广播ACTION_DOWNLOAD_COMPLETE中,可拿到download_id |
COLUMN_REASON | 提供有关下载状态的更多详细信息 |
COLUMN_STATUS | 当前的下载状态,经过DownloadManager.Query来查询 |
STATUS_PENDING | 下载开始 |
STATUS_RUNNING | 下载进行中 |
STATUS_PAUSED | 下载暂停,这里会等待重试,注意这是断点续传,暂停缘由能够经过COLUMN_REASON去查 |
STATUS_SUCCESSFUL | 下载成功 |
STATUS_FAILED | 下载失败,这里的失败不会重试的,缘由能够经过COLUMN_REASON去查 |
enqueue(DownloadManager.Request request) | 开启一个下载服务 |
getMaxBytesOverMobile(Context context) | 返回手机移动网络限定下载的最大值 |
getMimeTypeForDownloadedFile(long id) | 经过download_id查询下载文件的媒体类型,也就是格式 |
getRecommendedMaxBytesOverMobile(Context context) | 获取建议的移动网络下载的大小 |
getUriForDownloadedFile(long id) | 若是文件下载成功,返回文件的Uri |
openDownloadedFile(long id) | 打开下载的文件,读文件 |
query(DownloadManager.Query query) | 下载查询 |
remove(long... ids) | 取消下载并从下载管理器中删除文件 |
以上即是DownloadManager下载使用到的核心api了,基本上知足一个正常的下载了,固然并无所有罗列出来,像下载暂停和下载失败关于COLUMN_REASON
的描述 还有不少,就不罗列出来了,下面看看下载更新的代码片断:安全
// 获取下载管理器
downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
clearCurrentTask();
// 下载地址若是为null,抛出异常
String downloadUrl = Objects.requireNonNull(appUpdate.getNewVersionUrl());
Uri uri = Uri.parse(downloadUrl);
DownloadManager.Request request = new DownloadManager.Request(uri);
// 下载中和下载完成显示通知栏
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (TextUtils.isEmpty(appUpdate.getSavePath())) {
//使用系统默认的下载路径 此处为应用内 /android/data/packages ,因此兼容7.0
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, context.getPackageName() + ".apk");
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS + File.separator + context.getPackageName() + ".apk")));
} else {
// 自定义的下载目录,注意这是涉及到android Q的存储权限,建议不要用getExternalStorageDirectory()
request.setDestinationInExternalFilesDir(context, appUpdate.getSavePath(), context.getPackageName() + ".apk");
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(appUpdate.getSavePath() + File.separator + context.getPackageName() + ".apk")));
}
// 设置通知栏的标题
request.setTitle(getAppName());
// 设置通知栏的描述
request.setDescription("正在下载中...");
// 设置媒体类型为apk文件
request.setMimeType("application/vnd.android.package-archive");
// 开启下载,返回下载id
lastDownloadId = downloadManager.enqueue(request);
// 如须要进度及下载状态,增长下载监听
if (!appUpdate.getIsSlentMode()) {
DownloadHandler downloadHandler = new DownloadHandler(this);
downloadObserver = new DownloadObserver(downloadHandler, downloadManager, lastDownloadId);
context.getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true, downloadObserver);
}
复制代码
Cursor cursor = downloadManager.query(query);
if (cursor != null && cursor.moveToNext()) {
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
int totalSize = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
int currentSize = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
// 当前进度
int mProgress;
if (totalSize != 0) {
mProgress = (currentSize * 100) / totalSize;
} else {
mProgress = 0;
}
Log.d(TAG,String.valueOf(mProgress));
switch (status) {
case DownloadManager.STATUS_PAUSED:
// 下载暂停
handler.sendEmptyMessage(DownloadManager.STATUS_PAUSED);
Log.d(TAG,"STATUS_PAUSED");
break;
case DownloadManager.STATUS_PENDING:
// 开始下载
handler.sendEmptyMessage(DownloadManager.STATUS_PENDING);
Log.d(TAG,"STATUS_PENDING");
break;
case DownloadManager.STATUS_RUNNING:
// 正在下载,不作任何事情
Message message = new Message();
message.what = DownloadManager.STATUS_RUNNING;
message.arg1 = mProgress;
handler.sendMessage(message);
Log.d(TAG,"STATUS_RUNNING");
break;
case DownloadManager.STATUS_SUCCESSFUL:
if(!isEnd){
// 完成
handler.sendEmptyMessage(DownloadManager.STATUS_SUCCESSFUL);
Log.d(TAG,"STATUS_SUCCESSFUL");
}
isEnd = true;
break;
case DownloadManager.STATUS_FAILED:
if(!isEnd){
handler.sendEmptyMessage(DownloadManager.STATUS_FAILED);
Log.d(TAG,"STATUS_FAILED");
}
isEnd = true;
break;
default:
Log.d(TAG,"default");
break;
}
cursor.close();
} else {
Log.d(TAG,"cursor======null");
}
复制代码
android 6.0 版本引入了一种新的权限模式,现在,用户可直接在运行时管理应用权限。这种模式让用户可以更好地了解和控制权限,同时为应用开发者精简了安装和自动更新过程。用户可为所安装的各个应用分别授予或撤销权限。性能优化
对于以 Android 6.0(API级别23)或更高版本为目标平台的应用,请务必在运行时检查和请求权限。要肯定您的应用是否已被授予权限,请调用新增的checkSelfPermission()方法。要请求权限,请调用新增的requestPermissions()
方法。即便您的应用并不以Android6.0(API级别23)为目标平台,您也应该在新权限模式下测试您的应用.官方传送门bash
因为下载须要读写文件,Android M 须要动态申请运行时权限,关于如何查看运行时权限,能够经过AndroidStudio的Terminal终端执行以下命令:
$ adb shell pm list permissions -d -g
$ adb shell pm [grant|revoke] ...
$ adb shell pm list permissions -s
M运行时权限请求代码片断:
/**
* 判断存储卡权限
*/
private void requestPermission() {
//权限判断是否有访问外部存储空间权限
int flag = ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (flag != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// 用户拒绝过这个权限了,应该提示用户,为何须要这个权限。
Toast.makeText(getActivity(), getResources().getString(R.string.update_permission), Toast.LENGTH_LONG).show();
}
// 申请受权
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
// 拥有权限,执行下载相关逻辑
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 授予权限,执行下载相关逻辑
} else {
//拒绝权限,给出提示
Toast.makeText(getActivity(), getResources().getString(R.string.update_permission), Toast.LENGTH_LONG).show();
dismiss();
}
}
}
}
复制代码
为了提升私有文件的安全性,面向Android7.0或更高版本的应用私有目录被限制访问(0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。此权限更改有多重反作用:
MODE_WORLD_READABLE
和/或 MODE_WORLD_WRITEABLE
而进行的此类尝试将触发 SecurityException
。注:迄今为止,这种限制尚不能彻底执行。应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。可是,咱们强烈反对放宽私有目录的权限
FileUriExposedException
。分享私有文件内容的推荐方法是使用 FileProvider。COLUMN_LOCAL_FILENAME
时可能出现没法访问的路径。面向Android7.0或更高版本的应用在尝试访问COLUMN_LOCAL_FILENAME
时会触发SecurityException
。经过使用DownloadManager.Request.setDestinationInExternalFilesDir()
或DownloadManager.Request.setDestinationInExternalPublicDir()
将下载位置设置为公共位置的旧版应用仍能够访问COLUMN_LOCAL_FILENAME
中的路径,可是咱们强烈反对使用这种方法。对于由DownloadManager公开的文件,首选的访问方式是使用ContentResolver.openFileDescriptor()
。<provider
android:name=".DownloadFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_file_path" />
</provider>
复制代码
<paths>
<external-path
name="external_storage_root"
path="." />
<files-path
name="files-path"
path="." />
<cache-path
name="cache-path"
path="." />
<!--/storage/emulated/0/Android/data/...-->
<external-files-path
name="external_file_path"
path="." />
<!--表明app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的目录-->
<external-cache-path
name="external_cache_path"
path="." />
<!--配置root-path。这样子能够读取到sd卡和一些应用分身的目录,听说应用分身有bug-->
<root-path
name="root-path"
path="" />
/paths>
复制代码
File downloadFile = getDownloadFile();
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
intent.setDataAndType(Uri.fromFile(downloadFile), "application/vnd.android.package-archive");
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
boolean allowInstall = context.getPackageManager().canRequestPackageInstalls();
if (!allowInstall) {
//不容许安装未知来源应用,请求安装未知应用来源的权限
if (mainPageExtraListener != null) {
mainPageExtraListener.applyAndroidOInstall();
}
return;
}
}
//Android7.0以后获取uri要用contentProvider
Uri apkUri = FileProvider.getUriForFile(context.getApplicationContext(), context.getPackageName() + ".fileProvider", downloadFile);
//Granting Temporary Permissions to a URI
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
复制代码
针对 8.0 的应用须要在 AndroidManifest.xml 中声明REQUEST_INSTALL_PACKAGES
权限,不然将没法进行应用内升级
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
复制代码
/**
* 检测到无权限安装未知来源应用,回调接口中须要从新请求安装未知应用来源的权限
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void applyAndroidOInstall() {
//请求安装未知应用来源的权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, INSTALL_PACKAGES_REQUESTCODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// 8.0的权限请求结果回调
if (requestCode == INSTALL_PACKAGES_REQUESTCODE) {
// 受权成功
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 执行安装apk的逻辑...
} else {
// 受权失败,引导用户去未知应用安装的界面
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
//注意这个是8.0新API
Uri packageUri = Uri.parse("package:" + getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri);
startActivityForResult(intent, GET_UNKNOWN_APP_SOURCES);
}
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//8.0应用设置界面未知安装开源返回时候
if (requestCode == GET_UNKNOWN_APP_SOURCES) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean allowInstall = getPackageManager().canRequestPackageInstalls();
if (allowInstall) {
// 执行安装app的逻辑...
} else {
// 拒绝权限逻辑...
Toast.makeText(MainActivity.this,"您拒绝了安装未知来源应用,应用暂时没法更新!",Toast.LENGTH_SHORT).show();
}
}
}
}
复制代码
目前Android Q
官网上仍是处于Beta版,Android Q
最大的变化 无非是对用户隐 私权的进一步保护,为每一个应用程序在外部存储设备提供了一个独立的存储沙箱,应用经过路径建立的文件都保存在应用的沙箱目录。
关于下载,文件确定须要保存到本地了,可是因为AndroidQ采起分区存储,导致:getExternalStorageDirectory()与getExternalStoragePublicDirectory()
读写权限变化,用户在拥有读写权限的同时,不能够在内部存储肆意的构建本身的目录,这样也更容易管理,卸载应用的时候也能够将这块数据与文件彻底删除。
if (TextUtils.isEmpty(appUpdate.getSavePath())) {
//使用系统默认的下载路径 此处为应用内 /android/data/packages ,因此兼容7.0
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, context.getPackageName() + ".apk");
} else {
// 自定义的下载目录,注意这是涉及到android Q的存储权限,建议不要用getExternalStorageDirectory()
request.setDestinationInExternalFilesDir(context, appUpdate.getSavePath(), context.getPackageName() + ".apk");
// 清除本地缓存的文件
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(appUpdate.getSavePath())));
}
复制代码
经过setDestinationInExternalFilesDir()存储文件与getExternalFilesDir()获取文件,彻底能够避免Android Q对于存储作出的限制。
若是采起系统的DownloadManager来实现更新的话,我的以为能够不用进行校验,固然若是惧怕下载的文件被篡改或者不完整的话建议能够加上MD5校验。关于MD5做用有如下几点:
下面查看一下代码片断:
/**
* 检查文件的MD5的合法性,若不一致,则没法安装
*
* @param md5 服务器返回的文件md5值
* @param file 下载的apk文件
* @return true 则md5校验经过 false 则失败
*/
public static boolean checkFileMd5(String md5, File file) {
if (TextUtils.isEmpty(md5)) {
return false;
}
String md5OfFile = getFileMd5ToString(file);
if (TextUtils.isEmpty(md5OfFile)) {
return false;
}
return md5.equalsIgnoreCase(md5OfFile);
}
/**
* Return the MD5 of file.
*
* @param file The file.
* @return the md5 of file
*/
private static String getFileMd5ToString(final File file) {
return bytes2HexString(getFileMd5(file));
}
private static final char[] HEX_DIGITS =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
private static String bytes2HexString(final byte[] bytes) {
if (bytes == null) {
return "";
}
int len = bytes.length;
if (len <= 0) {
return "";
}
char[] ret = new char[len << 1];
for (int i = 0, j = 0; i < len; i++) {
ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
}
return new String(ret);
}
/**
* Return the MD5 of file.
*
* @param file The file.
* @return the md5 of file
*/
private static byte[] getFileMd5(final File file) {
if (file == null) {
return null;
}
DigestInputStream dis = null;
try {
FileInputStream fis = new FileInputStream(file);
MessageDigest md = MessageDigest.getInstance("MD5");
dis = new DigestInputStream(fis, md);
byte[] buffer = new byte[1024 * 256];
while (true) {
if (dis.read(buffer) <= 0) {
break;
}
}
md = dis.getMessageDigest();
return md.digest();
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
} finally {
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
复制代码
关于版本更新大概就这么多知识点了,比较简单,可是很零碎,若是想要了解详细的内容,卿能够下载源码进行查看哦,源码详见个人开源地址AppUpdate,本库通过长期的验证,稳定性很OK的啦,若是有好的想法,直接提issues。