需求:
apk更新安装
关于下载,需要考虑的问题:
1.多任务下载
2.多线程下载
3.断点续传
4.高并发
方案:
- 自己封装URLConnection 连接请求类
- 使用第三方 okhttp 网络请求框架
- 使用第三方库liulishuo
- Android自定的下载管理(会在notification 显示下载的进度,同时可以暂停、重新连接等)
- 使用Android高能下载库FileDownloader
- AndroidDownloader 一个开源的多线程,多任务下载框架
- 等等……
(第三方下载库的,有很多很多,不赘述。下载着重介绍使用Android自带的DownloadManager进行下载和安装apk)
进入正题:
使用Android自带的DownloadManager进行下载,实际上是交付给了系统app来完成,这样的好处不会消耗自身APP的 CPU资源。因为系统分配给每个app的内存大小等都是有限制的。像目前很多封装好的下载的第三方库大多也都是会另开一个后台服务,更有甚者会设置这个服务开启一个新的进程。都是这个原因。
代码实现:
安装区别版本
protected static void installAPK(Context context, String absolutePath, String apkName) {
File apkFile = new File(absolutePath, apkName);//"版本名称"
Intent intent = new Intent(Intent.ACTION_VIEW);
try {
String[] command = {"chmod", "", absolutePath};
ProcessBuilder builder = new ProcessBuilder(command);
builder.start();
} catch (Exception ignored) {
ignored.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);//必须要用 自己包下面的fileprovider
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
监听器
public interface DownloadManagerListener {
void onPrepare();
void onSuccess(String path);
void onFailed(Throwable throwable);
}
apk下载管理类:
public class ApkDownloadManager {
public static final String APPLICATION_ID = "com.car.cartechpro.beta";
public static final String TAG = "下载eee:";
private DownloadManager downloadManager;
private Context context;
private long downloadId;
private String url;
private String name;
private String serverVersionName;
private String path;
private DownloadManagerListener listener;
public ApkDownloadManager(Context context, String url, String serverVersionName) {
this.context = context;
this.url = url;
this.name = getFileName(context, serverVersionName);
this.serverVersionName = serverVersionName;
}
/**
* Sets listener.
*
* @param listener the listener
* @return the listener
*/
public ApkDownloadManager setListener(DownloadManagerListener listener) {
this.listener = listener;
return this;
}
/**
* 开始下载
*/
public void download() {
//设置下载的路径
File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), name);
path = file.getAbsolutePath();
if(file.exists()){
//ToastUtil.toastText("已经下载过了:" + path);
Log.i(TAG, "已经下载过了:" + path);
file.delete();
}
//
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
//移动网络情况下是否允许漫游
request.setAllowedOverRoaming(false);
//在通知栏中显示,默认就是显示的
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setTitle(name);
request.setDescription("正在下载中......");
request.setVisibleInDownloadsUi(true);
request.setDestinationUri(Uri.fromFile(file));
//获取DownloadManager
if (downloadManager == null) {
downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
}
//将下载请求加入下载队列,加入下载队列后会给该任务返回一个long型的id,通过该id可以取消任务,重启任务、获取下载的文件等等
if (downloadManager != null) {
if (listener != null) {
listener.onPrepare();
}
downloadId = downloadManager.enqueue(request);
}
//注册广播接收者,监听下载状态
context.registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
//广播监听下载的各个状态
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
DownloadManager.Query query = new DownloadManager.Query();
//通过下载的id查找
query.setFilterById(downloadId);
Cursor cursor = downloadManager.query(query);
if (cursor.moveToFirst()) {
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
//下载暂停
case DownloadManager.STATUS_PAUSED:
break;
//下载延迟
case DownloadManager.STATUS_PENDING:
break;
//正在下载
case DownloadManager.STATUS_RUNNING:
break;
//下载完成
case DownloadManager.STATUS_SUCCESSFUL:
if (listener != null) {
listener.onSuccess(path);
}
cursor.close();
context.unregisterReceiver(receiver);
break;
//下载失败
case DownloadManager.STATUS_FAILED:
if (listener != null) {
listener.onFailed(new Exception("下载失败"));
}
cursor.close();
context.unregisterReceiver(receiver);
break;
}
}
}
};
/**
* 获取文件名
*
* @param
* @return
*/
private static final String getFileName(final Context context, final String serverVersionName) {
String filename = "";
String packageName = context.getPackageName();
filename = packageName + "_" + serverVersionName + ".apk";
return filename;
}
/**
* 获取下载地址
* @param
* @return
*/
private String getApkLocalPath(final Context context, final String serverVersionName){
String filePath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {//外部存储卡
filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
} else {
Log.i(TAG, "没有SD卡");
return "";
}
String apkLocalPath = filePath + File.separator + getFileName(context, serverVersionName) + ".apk";
return apkLocalPath;
}
/**
* Download.
*
* @param context the context
* @param url the url
* @param serverVersionName the server version name
*/
public static void downloadApk(final Context context, final String url, final String serverVersionName){
new ApkDownloadManager(context, url, serverVersionName)
.setListener(new DownloadManagerListener() {
@Override
public void onPrepare() {
Log.i(TAG, "onPrepare>>>>" + ";url:" + url);
}
@Override
public void onSuccess(String path) {
Log.i(TAG, "onSuccess >>>>" + path);
//ToastUtil.toastText("下载好了亲!");
installApk(context, path, getFileName(context, serverVersionName));
}
@Override
public void onFailed(Throwable throwable) {
Log.i(TAG, "onFailed", throwable);
}
})
.download();
}
}
安装:
protected static void installApk(Context context, String absolutePath, String apkName) {
File file = new File(absolutePath);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri data = FileProvider.getUriForFile(context, APPLICATION_ID +".provider",file);
intent.setDataAndType(data,"application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.startActivity(intent);
}
在AndroidManifest.xml中需要写
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
其中的@xml/file_paths,是标注了访问资源的路径范围。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
</paths>
注意:其中provider的authorities需要和上面函数中的APPLICATION_ID保持一致。
否则会报这样的错误:Couldn't find meta-data for provider with authority
(至于为什么需要使用到Provider,这个便涉及到Android中ContentProvider的相关知识点了,总的来说就是可以在不同的应用程序之间共享数据。而其中的Authority指:授权信息,用以区别不同的ContentProvider。)
另外,需要授权:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
进度监听
private DownloadContentObserver observer = new DownloadContentObserver();
private Handler handler = new Handler(Looper.getMainLooper());
private static final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads");
class DownloadContentObserver extends ContentObserver {
public DownloadContentObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
updateView();
}
}
public void updateView() {
int[] bytesAndStatus = getBytesAndStatus(downloadId);
int currentSize = bytesAndStatus[0];//当前大小
int totalSize = bytesAndStatus[1];//总大小
int status = bytesAndStatus[2];//下载状态
Log.e(TAG, "当前大小" + currentSize + "下载状态" + status + "总大小" +totalSize);
Log.e(TAG, "当前进度" + getProcess(currentSize, totalSize));
loadingDialog.setLoadingTitle(activity.getString(R.string.loading) + "\n" + getProcess(currentSize, totalSize));
Message.obtain(handler, 0, currentSize, totalSize, status).sendToTarget();
}
private String getProcess(int currentSize, int totalSize){
NumberFormat numberFormat = NumberFormat.getInstance();
numberFormat.setMaximumFractionDigits(2);
String result = numberFormat.format((float) currentSize / (float) totalSize * 100);
return result + "%";
}
public int[] getBytesAndStatus(long downloadId) {
int[] bytesAndStatus = new int[] { -1, -1, 0 };
DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
Cursor c = null;
try {
c = downloadManager.query(query);
if (c != null && c.moveToFirst()) {
bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
}
} finally {
if (c != null) {
c.close();
}
}
return bytesAndStatus;
}
Android app的applicationId和包名的区别
上述说到的provider的authorities,还需要特别注意的是,需要给予一个唯一识别的值。也就是说,必须是在整个系统(手机)中,有且仅有这一个。因此,一般会选择刚好有此特性的applicationId的值。
在AndroidManifest.xml中这样写:
android:authorities="${applicationId}.provider"
然后,在代码中,很多文章是写成了:
getApplication().getPackageName();
其实不然。这样拿到的其实只是包名,而我们如果想拿到在build.gradle中配置的 applicationId,并不能这么拿。
一般情况下,不做任何处理的话,这两个值是默认一致的。但是,大多数实际开发情况,我们会打不同的包,每个包的applicationId都会做处理,避免一样导致不能在同一系统中安装该项目的不同包。而这个时候如果依旧拿包名的话,app就会出现闪退的异常情况了。
因此,严谨上,应该使用下面这段代码。
getApplication().getApplicationInfo().processName;
借鉴的文章: