APK 在线升级

APK 在线升级几乎是所有程序必备的功能。

在线升级功能能解决已有的问题并提供更丰富的新功能。

基本的流程是:

  1. 检测到新版本信息
  2. 弹出升级提示窗口
  3. 点击 No 不进行升级,完毕!
  4. 点击 Yes 后台下载升级程序
  5. 程序下载完成进入安装页面
  6. 安装成功,进入新程序

下面将介绍使用 UpdateAppUtil 实现在线升级功能

0. 需要权限

需要授权 android.permission.INTERNETandroid.permission.WRITE_EXTERNAL_STORAGE 权限,具体申请版本在这里不展开了。

有兴趣的小伙伴可以看以往的文章。

1. 获取新版本信息

一般是访问服务器,获取新版本信息(以下面为例)


{
"url":"https://www.google.com/test/a670ef11/apk/test.apk",
"versionCode":1,
"versionName":"v2.1.0",
"create_time":"2019-12-14 03:44:34",
"description":"新增卫星链路,支持全球访问。"
}

必须要有 APK 的下载链接(url),版本号(versionCode)或者版本名(versionName)。

都是接下来需要用到的。

2. 设置信息


UpdateAppUtil.from(MainActivity.this)
.checkBy(UpdateAppUtil.CHECK_BY_VERSION_NAME) //更新检测方式,默认为VersionCode
.serverVersionCode(0)
.serverVersionName(version)
.updateInfo(description)
.apkPath(url)
.update();

字段 说明
checkBy 是否需要弹出升级提示的依据。CHECK_BY_VERSION_NAME 是根据 serverVersionName 的不同就弹出升级提示。CHECK_BY_VERSION_CODE 是根据 serverVersionCode 高于当前软件版本弹出升级提示。
serverVersionCode 设置新软件的 versionCode (如示例的 1 )
serverVersionName 设置新软件的 versionName (如示例的 "v2.1.0" )
updateInfo 升级提示窗口显示的新软件描述
apkPath 新软件下载链接(需要通过此链接下载新软件)
update 马上进行升级检查(如满足升级要求,弹出升级提示)
isForce 如果不选择升级,直接退出程序

3. 下载升级程序

Android 有多种框架可以下载程序(okhttp等),也可以开启一个线程去下载(IntentService)。

而 UpdateAppUtil 采用 Android SDK 提供的下载框架 DownloadManager


public static void downloadWithAutoInstall(Context context, String url, String fileName, String notificationTitle, String descriptInfo) {
if (TextUtils.isEmpty(url)) {
Log.e(TAG, "url为空!!!!!");
return;
}
try {
Uri uri = Uri.parse(url);
Log.i(TAG, String.valueOf(uri));
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(uri);
// 在通知栏中显示
request.setVisibleInDownloadsUi(true);
request.setTitle(notificationTitle);
request.setDescription(descriptInfo);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setMimeType("application/vnd.android.package-archive"); String filePath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {//SD卡是否正常挂载
filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
} else {
Log.i(TAG, "没有SD卡" + "filePath:" + context.getFilesDir().getAbsolutePath());
return;
} downloadUpdateApkFilePath = filePath + File.separator + fileName;
// 若存在,则删除
deleteFile(downloadUpdateApkFilePath);
Uri fileUri = Uri.parse("file://" + downloadUpdateApkFilePath);
request.setDestinationUri(fileUri);
downloadUpdateApkId = downloadManager.enqueue(request);
} catch (Exception e) {
e.printStackTrace();
}
}

request.setVisibleInDownloadsUi 下载UI显示到通知栏上

request.setTitle 设置通知栏的标题

request.setDescription 设置通知栏的消息

request.setNotificationVisibility 下载过程中一直显示下载信息,下载完后也存在(直到用户消除)

会清除没完成的文件,重新下载。

DownloadManager 下载过程中(下载完成)会发出广播,想要对下载完成进行处理需要监听广播。

(downloadUpdateApkFilePath 保存下载文件的路径,下载完成后可以通过此进行安装)

4. 进入安装

DownloadManager 下载完成后会发出 ACTION_DOWNLOAD_COMPLETE


public class UpdateAppReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Cursor cursor = null;
try {
if (! intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
return;
}
if (DownloadAppUtil.downloadUpdateApkId <= 0) {
return;
}
long downloadId = DownloadAppUtil.downloadUpdateApkId;
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
cursor = manager.query(query);
if (cursor.moveToNext()) {
int staus = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
if (staus == DownloadManager.STATUS_FAILED) {
manager.remove(downloadId);
} else if ((staus == DownloadManager.STATUS_SUCCESSFUL)
&& (DownloadAppUtil.downloadUpdateApkFilePath != null)) {
Intent it = new Intent(Intent.ACTION_VIEW);
it.setDataAndType(Uri.parse("file://" + DownloadAppUtil.downloadUpdateApkFilePath),
"application/vnd.android.package-archive");
// todo 针对不同的手机 以及 sdk 版本, 这里的 uri 地址可能有所不同
it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(it);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}

判断是 DownloadManager.ACTION_DOWNLOAD_COMPLETE 获取 APK 路径进行安装。

一、前言

app在线更新是一个比较常见需求,新版本发布时,用户进入我们的app,就会弹出更新提示框,第一时间更新新版本app。在线更新分为以下几个步骤:

 1, 通过接口获取线上版本号,versionCode
2, 比较线上的versionCode 和本地的versionCode,弹出更新窗口
3, 下载APK文件(文件下载)
4,安装APK

在线更新就上面几个步骤,前2步比较简单,重要的就是后2个步骤,而由于Android 各个版本对权限和隐私的收归和保护,因此,会出现各种的适配问题,因此本文就总结一下app 在线更新方法和遇到的一些适配问题。

二、apk 下载

apk下载其实就是文件下载,而文件下载有很多方式:

  1,很多三方框架都有文件上传下载功能,可以借助三方框架(比如Volley,OkHttp)
2,也可以开启一个线程去下载,(可以用IntentService)
3,最简单的一种方式:Android SDK 其实给我们提供了下载类DownloadManager,只需要简单的配置项设置,就能轻松实现下载功能。

本文就用第三种方式,用 DownloadManager 来下载apk。

1. 使用DownloadManager 下载apk

DownloadManager 是SDK 自带的,大概流程如下:

(1)创建一个Request,进行简单的配置(下载地址,和文件保存地址等)
(2)下载完成后,系统会发送一个下载完成的广播,我们需要监听广播。
(3)监听到下载完成的广播后,根据id查找下载的apk文件
(4)在代码中执行apk安装。

public void downloadApk(String apkUrl, String title, String desc) {
// fix bug : 装不了新版本,在下载之前应该删除已有文件
File apkFile = new File(weakReference.get().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk"); if (apkFile != null && apkFile.exists()) { apkFile.delete(); } DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//设置title
request.setTitle(title);
// 设置描述
request.setDescription(desc);
// 完成后显示通知栏
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalFilesDir(weakReference.get(), Environment.DIRECTORY_DOWNLOADS, "test.apk");
//在手机SD卡上创建一个download文件夹
// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
//指定下载到SD卡的/download/my/目录下
// request.setDestinationInExternalPublicDir("/codoon/","test.apk"); request.setMimeType("application/vnd.android.package-archive");
//记住reqId
mReqId = mDownloadManager.enqueue(request);
}

如上代码所示,首先构建一个Request,设置下载地址,标题、描述、apk存放目录等,最后,调用mDownloadManager.enqueue(request) 开始下载。

注意:这里我们需要记住这个mReqId,因为下载完成之后,我们需要根据这个ID 去查找apk文件,然后安装apk.

2.更新下载进度

下载文件,我们一般需要知道下载的进度,在界面给用户一个友好的提示,app 更新也是一样,我们需要在界面上显示当前下载进度和总进度,让用户知道大概会等待多久。那么如果获取下载进度呢?

在下载之前,我们需要在Activity 中注册一个Observer,就是一个观察者,当下载进度变化的时候,就会通知观察者,从而更新进度。步骤如下:

1, 首先我们先定义一个观察者DownloadChangeObserver来观察下载进度
2,在DownloadChangeObserver 中更新UI进度,给用户提示
3,下载之前,在Activity 中注册Observer

具体代码如下:
DownloadChangeObserver.class:

 class DownloadChangeObserver extends ContentObserver {

        /**
* Creates a content observer.
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
public DownloadChangeObserver(Handler handler) {
super(handler);
} @Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
updateView();
}
}

updateView()方法中,查询下载进度。

 private void updateView() {
int[] bytesAndStatus = new int[]{0, 0, 0};
DownloadManager.Query query = new DownloadManager.Query().setFilterById(mReqId);
Cursor c = null;
try {
c = mDownloadManager.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();
}
} if (mUpdateListener != null) {
mUpdateListener.update(bytesAndStatus[0], bytesAndStatus[1]);
} Log.i(TAG, "下载进度:" + bytesAndStatus[0] + "/" + bytesAndStatus[1] + "");
}

根据前面我们记录的ID去查询进度,代码中已经注释了,不再多讲。

要想获取到进度,在下载之前,还得先注册DownloadChangeObserver,代码如下:

weakReference.get().getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true,
mDownLoadChangeObserver);

3. 获取下载结果

DownloadManager在下载完成之后,会发送一个下载完成的广播DownloadManager.ACTION_DOWNLOAD_COMPLETE,我们只需要监听这个广播,收到广播后, 获取apk文件安装。

定义一个广播DownloadReceiver

class DownloadReceiver extends BroadcastReceiver {

        @Override
public void onReceive(final Context context, final Intent intent) {
// 安装APK
long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); Logger.e(TAG, "收到广播");
Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW); if (completeDownLoadId == mReqId) {
uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
} intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
} }

在下载之前注册广播

 // 注册广播,监听APK是否下载完成
weakReference.get().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));

通过上面的几个步骤,基本上就完成app在线更新功能,在Android 6.0以下可以正常运行。但是别忙,本文还没有结束,Android每一个版本都有一些改动,导致我们需要适配不同的版本,不然的话,就会出问题,结下来就看一下Android 6.0,7.0,8.0 的相关适配。

三、适配Android 6.0

通过前面讲的几个步骤,app 在线更新在6.0以下已经可以正常运行,在Android6.0上,安装的时候会报出以下错误:

 
Android6.0错误.png
Caused by:
5 android.content.ActivityNotFoundException:No Activity found to handle Intent { act=android.intent.action.VIEW typ=application/vnd.android.package-archive flg=0x10000000 }

为什么会报上面的错误,经过debug发现,在Android6.0以下和Android6.0上,通过DownloadManager 获取到的Uri不一样。

区别如下:(1)Android 6.0,getUriForDownloadedFile得到 值为: content://downloads/my_downloads/10
(2) Android6.0以下,getUriForDownloadedFile得到的值为:file:///storage/emulated/0/Android/data/packgeName/files/Download/xxx.apk

可以看到,Android6.0得到的apk地址为:content:// 开头的一个地址,安装的时候就会报上面的错误。怎么解决呢?经过查找资料找到了解决办法:

   //通过downLoadId查询下载的apk,解决6.0以后安装的问题
public static File queryDownloadedApk(Context context, long downloadId) {
File targetApkFile = null;
DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); if (downloadId != -1) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
Cursor cur = downloader.query(query);
if (cur != null) {
if (cur.moveToFirst()) {
String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (!TextUtils.isEmpty(uriString)) {
targetApkFile = new File(Uri.parse(uriString).getPath());
}
}
cur.close();
}
}
return targetApkFile;
}

代码如上所示,不通过getUriForDownloadedFile去获取Uri,通过DownloadManager.COLUMN_LOCAL_URI 这个字段去获取apk地址。

适配Android 6.0后,安装apk 的代码如下:

 /**
* @param context
* @param intent
*/
private void installApk(Context context, Intent intent) {
long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); Logger.e(TAG, "收到广播");
Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW); if (completeDownLoadId == mReqId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
File apkFile = queryDownloadedApk(context, completeDownLoadId);
uri = Uri.fromFile(apkFile);
} // 安装应用
Logger.e("zhouwei", "下载完成了"); intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
}
四、适配Android 7.0

刚适配完6.0,在7.0以上的机子上又出问题了,为什么呢?因为在Android 7.0上,对文件的访问权限作出了修改,不能在使用file://格式的Uri 访问文件 ,Android 7.0提供 FileProvider,应该使用这个来获取apk地址,然后安装apk。如下进行简单的适配:

(1) 在res 目录下,新建一个xml 文件夹,在xml 下面创建一个文件provider_paths文件:

<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="" />
<external-files-path
name="Download"
path="" />
</paths>

(2) 在AndroidManifest.xml清单文件中申明Provider:

<!-- Android 7.0 照片、APK下载保存路径-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="packgeName.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>

(3) Android 7.0上的文件地址获取:

 uri = FileProvider.getUriForFile(context,
"packageNam.fileProvider",
new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "xxx.apk"));

好了,就这样7.0适配工作就完成了,适配后的安装代码如下:

 /**
* @param context
* @param intent
*/
private void installApk(Context context, Intent intent) {
long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); Logger.e(TAG, "收到广播");
Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW); if (completeDownLoadId == mReqId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
File apkFile = queryDownloadedApk(context, completeDownLoadId);
uri = Uri.fromFile(apkFile);
} else { // Android 7.0 以上
uri = FileProvider.getUriForFile(context,
"packgeName.fileProvider",
new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "xxx.apk"));
intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} // 安装应用
Logger.e("zhouwei", "下载完成了"); intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
}

注意:把上面的packageNam 换成你自己的包名,把xxx.apk 换成你自己的apk的名字。
关于更多FileProvider的东西,这儿就不展开讲了,想要了解的可以看一下鸿洋的文章:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧
,讲的很清楚。

五、适配Android 8.0:未知来源的应用权限

好特么累,继续适配Android 8.0, 由于没有Android 8.0的手机,一直没有注意,前些天一个华为用户反馈在线更新不了新版本,具体表现就是:apk下载完成,一闪而过,没有跳转到apk安装界面。经过排查,确定了是Android 8.0权限问题。

Android8.0以上,未知来源的应用是不可以通过代码来执行安装的(在sd卡中找找到apk,手动安装是可以的),未知应用安装权限的开关被除掉,取而代之的是未知来源应用的管理列表,需要列表里面开启你的应用的未知来源的安装权限。Google这么做是为了防止一开始正经的应用后来开始通过升级来做一些不合法的事情,侵犯用户权益。

知道问题了,我们就适配吧:

(1) 在清单文件中申明权限:REQUEST_INSTALL_PACKAGES

  <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

(2) 在代码中判断用户是否已经受过权限了,如果已经授权,可以直接安装,如果没有授权,则跳转到授权列表,让用户开启未知来源应用安装权限,开启后,再安装应用。

在监听apk下载状态的广播中添加如下代码:

boolean haveInstallPermission;
// 兼容Android 8.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //先获取是否有安装未知来源应用的权限
haveInstallPermission = context.getPackageManager().canRequestPackageInstalls();
if (!haveInstallPermission) {//没有权限
// 弹窗,并去设置页面授权
final AndroidOInstallPermissionListener listener = new AndroidOInstallPermissionListener() {
@Override
public void permissionSuccess() {
installApk(context, intent);
} @Override
public void permissionFail() {
ToastUtils.shortToast(context, "授权失败,无法安装应用");
}
}; AndroidOPermissionActivity.sListener = listener;
Intent intent1 = new Intent(context, AndroidOPermissionActivity.class);
context.startActivity(intent1); } else {
installApk(context, intent);
}
} else {
installApk(context, intent);
}

因为授权时需要弹框提示,我们用一个Activity来代理创建了一个Activity:AndroidOPermissionActivity 来申请权限,用户点击设置后,跳转到权限设置界面,然后我们再onActivityResult 里判断是都授权成功。

AndroidOPermissionActivity 代码如下:

/**
* 兼容Android 8。0 APP 在线更新,权限申请界面
* Created by zhouwei on 2018/3/23.
*/ public class AndroidOPermissionActivity extends BaseActivity {
public static final int INSTALL_PACKAGES_REQUESTCODE = 1;
private AlertDialog mAlertDialog;
public static AppDownloadManager.AndroidOInstallPermissionListener sListener; @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // 弹窗 if (Build.VERSION.SDK_INT >= 26) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, INSTALL_PACKAGES_REQUESTCODE);
} else {
finish();
} } @RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case INSTALL_PACKAGES_REQUESTCODE:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (sListener != null) {
sListener.permissionSuccess();
finish();
}
} else {
//startInstallPermissionSettingActivity();
showDialog();
}
break; }
} private void showDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.app_name);
builder.setMessage("为了正常升级 xxx APP,请点击设置按钮,允许安装未知来源应用,本功能只限用于 xxx APP版本升级");
builder.setPositiveButton("设置", new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onClick(DialogInterface dialogInterface, int i) {
startInstallPermissionSettingActivity();
mAlertDialog.dismiss();
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if (sListener != null) {
sListener.permissionFail();
}
mAlertDialog.dismiss();
finish();
}
});
mAlertDialog = builder.create();
mAlertDialog.show();
} @RequiresApi(api = Build.VERSION_CODES.O)
private void startInstallPermissionSettingActivity() {
//注意这个是8.0新API
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 1);
} @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1 && resultCode == RESULT_OK) {
// 授权成功
if (sListener != null) {
sListener.permissionSuccess();
}
} else {
// 授权失败
if (sListener != null) {
sListener.permissionFail();
}
}
finish();
} @Override
protected void onDestroy() {
super.onDestroy();
sListener = null;
}
}

注意:当通过Intent 跳转到未知应用授权列表的时候,一定要加上包名,这样就能直接跳转到你的app下,不然只能跳转到列表。

 
跳转未知来源应用授权.png

好了,这样Android 8.0 上也可以在线更新了。

六、完整代码,封装了一个类AppDownloadManager

为了不依赖于某个Activity ,因此封装了一个AppDownloadManager
,少量几行代码就可以实现在线更新,给出完整代码:

public class AppDownloadManager {
public static final String TAG = "AppDownloadManager";
private WeakReference<Activity> weakReference;
private DownloadManager mDownloadManager;
private DownloadChangeObserver mDownLoadChangeObserver;
private DownloadReceiver mDownloadReceiver;
private long mReqId;
private OnUpdateListener mUpdateListener; public AppDownloadManager(Activity activity) {
weakReference = new WeakReference<Activity>(activity);
mDownloadManager = (DownloadManager) weakReference.get().getSystemService(Context.DOWNLOAD_SERVICE);
mDownLoadChangeObserver = new DownloadChangeObserver(new Handler());
mDownloadReceiver = new DownloadReceiver();
} public void setUpdateListener(OnUpdateListener mUpdateListener) {
this.mUpdateListener = mUpdateListener;
} public void downloadApk(String apkUrl, String title, String desc) {
// fix bug : 装不了新版本,在下载之前应该删除已有文件
File apkFile = new File(weakReference.get().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_name.apk"); if (apkFile != null && apkFile.exists()) {
apkFile.delete();
} DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//设置title
request.setTitle(title);
// 设置描述
request.setDescription(desc);
// 完成后显示通知栏
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalFilesDir(weakReference.get(), Environment.DIRECTORY_DOWNLOADS, "app_name.apk");
//在手机SD卡上创建一个download文件夹
// Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
//指定下载到SD卡的/download/my/目录下
// request.setDestinationInExternalPublicDir("/codoon/","codoon_health.apk"); request.setMimeType("application/vnd.android.package-archive");
//
mReqId = mDownloadManager.enqueue(request);
} /**
* 取消下载
*/
public void cancel() {
mDownloadManager.remove(mReqId);
} /**
* 对应 {@link Activity }
*/
public void resume() {
//设置监听Uri.parse("content://downloads/my_downloads")
weakReference.get().getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true,
mDownLoadChangeObserver);
// 注册广播,监听APK是否下载完成
weakReference.get().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
} /**
* 对应{@link Activity#onPause()} ()}
*/
public void onPause() {
weakReference.get().getContentResolver().unregisterContentObserver(mDownLoadChangeObserver);
weakReference.get().unregisterReceiver(mDownloadReceiver);
} private void updateView() {
int[] bytesAndStatus = new int[]{0, 0, 0};
DownloadManager.Query query = new DownloadManager.Query().setFilterById(mReqId);
Cursor c = null;
try {
c = mDownloadManager.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();
}
} if (mUpdateListener != null) {
mUpdateListener.update(bytesAndStatus[0], bytesAndStatus[1]);
} Log.i(TAG, "下载进度:" + bytesAndStatus[0] + "/" + bytesAndStatus[1] + "");
} class DownloadChangeObserver extends ContentObserver { /**
* Creates a content observer.
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
public DownloadChangeObserver(Handler handler) {
super(handler);
} @Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
updateView();
}
} class DownloadReceiver extends BroadcastReceiver { @Override
public void onReceive(final Context context, final Intent intent) {
boolean haveInstallPermission;
// 兼容Android 8.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //先获取是否有安装未知来源应用的权限
haveInstallPermission = context.getPackageManager().canRequestPackageInstalls();
if (!haveInstallPermission) {//没有权限
// 弹窗,并去设置页面授权
final AndroidOInstallPermissionListener listener = new AndroidOInstallPermissionListener() {
@Override
public void permissionSuccess() {
installApk(context, intent);
} @Override
public void permissionFail() {
ToastUtils.shortToast(context, "授权失败,无法安装应用");
}
}; AndroidOPermissionActivity.sListener = listener;
Intent intent1 = new Intent(context, AndroidOPermissionActivity.class);
context.startActivity(intent1); } else {
installApk(context, intent);
}
} else {
installApk(context, intent);
} }
} /**
* @param context
* @param intent
*/
private void installApk(Context context, Intent intent) {
long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); Logger.e(TAG, "收到广播");
Uri uri;
Intent intentInstall = new Intent();
intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentInstall.setAction(Intent.ACTION_VIEW); if (completeDownLoadId == mReqId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
File apkFile = queryDownloadedApk(context, completeDownLoadId);
uri = Uri.fromFile(apkFile);
} else { // Android 7.0 以上
uri = FileProvider.getUriForFile(context,
"package_name.fileProvider",
new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_name.apk"));
intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} // 安装应用
Logger.e("zhouwei", "下载完成了"); intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intentInstall);
}
} //通过downLoadId查询下载的apk,解决6.0以后安装的问题
public static File queryDownloadedApk(Context context, long downloadId) {
File targetApkFile = null;
DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); if (downloadId != -1) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
Cursor cur = downloader.query(query);
if (cur != null) {
if (cur.moveToFirst()) {
String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
if (!TextUtils.isEmpty(uriString)) {
targetApkFile = new File(Uri.parse(uriString).getPath());
}
}
cur.close();
}
}
return targetApkFile;
} public interface OnUpdateListener {
void update(int currentByte, int totalByte);
} public interface AndroidOInstallPermissionListener {
void permissionSuccess(); void permissionFail();
}
}

使用就很简单了,如下:

(1) 弹出更新提示框:提示用户更新

 private void showUpdateDialog(final AppUpdateInfo updateInfo) {
AppUpdateDialog dialog = new AppUpdateDialog(getContext());
dialog.setAppUpdateInfo(updateInfo);
dialog.setOnUpdateClickListener(new AppUpdateDialog.OnUpdateClickListener() {
@Override
public void update(final AppUpdateDialog updateDialog) {
String title = "app name";
String desc = "版本更新"; mDownloadManager.setUpdateListener(new AppDownloadManager.OnUpdateListener() {
@Override
public void update(int currentByte, int totalByte) {
updateDialog.setProgress(currentByte, totalByte);
if ((currentByte == totalByte) && totalByte != 0) {
updateDialog.dismiss();
} }
}); mDownloadManager.downloadApk(updateInfo.download_url, title, desc);
} });
dialog.setCanceledOnTouchOutside(false);
dialog.setCancelable(false);
dialog.show(); }

(2) 注意在 onResume 和 onPause 调用对应方法:

 @Override
public void onResume() {
super.onResume();
if (mDownloadManager != null) {
mDownloadManager.resume();
}
}
 @Override
public void onPause() {
super.onPause();
if (mDownloadManager != null) {
mDownloadManager.onPause();
} }
 
app更新.png

七、总结

本文总结了项目中app在线更新遇到的一些适配问题,关于Android 6.0 的适配,如果你没有使用DownloadManager,可能不会遇到这个问题。7.0 和 8.0 的适配不管用哪种方式,都会有。

关于app在线更新版本适配就此结束,如果有啥问题,欢迎指出。

2018.9.4 更新

评论区有很多要AppUpdateDialog代码,其实没啥参考的,就一个自定义Dialog,我还是贴出来吧

public class AppUpdateDialog extends HeaderBaseDialog implements View.OnClickListener {
private String mContent;
private AppUpdateInfo mAppUpdateInfo;
private OnUpdateClickListener mOnUpdateListener;
private ProgressBar mProgressBar;
private TextView mValueText;
private boolean mForceUpdate = true; public AppUpdateDialog(@NonNull Context context) {
super(context);
} public void setContent(String mContent) {
this.mContent = mContent;
} public void setAppUpdateInfo(AppUpdateInfo mAppUpdateInfo) {
this.mAppUpdateInfo = mAppUpdateInfo;
} public void setProgress(int progress, int maxValue) {
if (maxValue == 0) {
return;
}
mProgressBar.setMax(maxValue);
mProgressBar.setProgress(progress);
mValueText.setText((int) (progress * 1.0f / maxValue * 100) + "%");
} public void setOnUpdateClickListener(OnUpdateClickListener mOnUpdateListener) {
this.mOnUpdateListener = mOnUpdateListener;
} @Override
protected void initView() {
setCanceledOnTouchOutside(false);
if (mAppUpdateInfo.force_update) {
mForceUpdate = true;
} else {
mForceUpdate = false;
}
TextView textView = (TextView) findViewById(R.id.app_update_content);
TextView version = (TextView) findViewById(R.id.app_update_version);
mProgressBar = (ProgressBar) findViewById(R.id.app_update_progress);
mValueText = (TextView) findViewById(R.id.app_update_current_percent);
version.setText(mAppUpdateInfo.version + " 更新内容");
textView.setText(mAppUpdateInfo.note);
TextView btnUpdate = (TextView) findViewById(R.id.btn_app_update);
btnUpdate.setText("更新");
btnUpdate.setOnClickListener(this); } @Override
protected int getHeaderLayout() {
return R.layout.app_update_header_layout;
} @Override
protected int getContentLayout() {
return R.layout.app_update_content_layout;
} @Override
protected Drawable getBgDrawable() {
return null;
} @Override
protected int getStartColor() {
return getContext().getResources().getColor(R.color.body_weight_start_color);
} @Override
protected int getEndColor() {
return getContext().getResources().getColor(R.color.body_weight_end_color);
} @Override
protected boolean isShowClose() {
return !mForceUpdate;
} @Override
public void onClick(View v) {
// update
if (mOnUpdateListener != null) {
findViewById(R.id.update_progress_layout).setVisibility(View.VISIBLE);
findViewById(R.id.btn_app_update).setVisibility(View.GONE);
mOnUpdateListener.update(this);
}
} public interface OnUpdateClickListener {
void update(AppUpdateDialog dialog);
}
}

Android apk在线升级的更多相关文章

  1. Android OTA在线升级一(架构分析)【转】

    本文转载自:http://blog.csdn.net/yanleizhouqing/article/details/50234213 1.前言 OTA(Over-the-Air Technology) ...

  2. android apk程序升级

    1 .设置apk版本号 Androidmanifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/ ...

  3. Android OTA在线升级二(升级包编译原理分析) 【转】

    本文转载自:http://blog.csdn.net/huryjiang/article/details/7590015 1 升级包的制作 基本命令: Ø  makeMtk [project[flav ...

  4. android 在线升级借助开源中国App源码

    android 在线升级借助开源中国App源码 http://www.cnblogs.com/luomingui/p/3949429.html android 在线升级借助开源中国App源码分析如下: ...

  5. android检查自动升级

    好像大半年没发点啥了,也不知道自己瞎忙啥,闲下来发给最近的东东<安卓在线升级> 类已经封装好了,简单的调用就OK了.这里的数据交互,我是用.NET写的一个webservice来交互.废话也 ...

  6. 实现乐鑫esp8266的无线OTA升级,实现远程在线升级固件

    代码地址如下:http://www.demodashi.com/demo/12994.html 一.前言: 写了这么多的8266博文,一直以满意100%的心态去敲写代码固件烧录,以致很少出现 bug ...

  7. 安卓APP在线升级

    安卓APP在线升级 通过IDHTTP组件在线下载APP到手机中,然后自动安装这个APP程序. 1)在线下载APP程序 需引用单元: {$IFDEF ANDROID} FMX.Helpers.Andro ...

  8. android apk 防止反编译技术第四篇-对抗JD-GUI

    又到周末一个人侘在家里无事可干,这就是程序员的悲哀啊.好了我们利用周末的时间继续介绍android apk防止反编译技术的另一种方法.前三篇我们讲了加壳技术(http://my.oschina.net ...

  9. android apk 自我保护技术-加密apk

    经过了忙碌的一周终于有时间静下来写点东西了,我们继续介绍android apk防止反编译技术的另一种方法.前两篇我们讲了加壳技术(http://my.oschina.net/u/2323218/blo ...

随机推荐

  1. 在centOS 7 上部署ansible自动化运维环境(01)

    环境:         3台centos 7        mycat : 10.0.0.2 mariadb1: 10.0.0.3 mariadb2: 10.0.0.4        为了实验方便 f ...

  2. HTTPS安全通信过程

         前言:本文是的第三篇文章.第一篇文章<常见加密算法特点及适用场景>,介绍了常见加密算法及其适用的场景,对加密算法做一个总体的概述.第二篇文章<非对称加密算法-RSA算法&g ...

  3. 两种方式测试 GNS3 环境

    GNS3已经部署好了,怎么测试环境呢?两种方式,一是使用自带的VPC连接交换机互联互通,二是配合VMware连接GNS3中的交换机互联互通. 自带 VPC 测试 使用两台VPC与一台二层交换机相连,测 ...

  4. linux档案和目录的管理

    资料来自鸟哥的linux私房菜,记录下来供自己平常使用参考 一:目录和路径: cd:change direcoty,变换目录的意思,就是从一个目录变到另一个目录,然后可以用绝对路径去变换目录,也可以用 ...

  5. JSON的Go解析

    JSON(Javascript Object Notation)是一种轻量级的数据交换语言,以文字为基础,具有自我描述性且易于让人阅读.尽管JSON是Javascript的一个子集,但JSON是独立于 ...

  6. react-native npm install

    --create project react-native init myapp --version 0.55.4 cd myapp -- react ui npm i react-native-el ...

  7. Maven之setting.xml 配置详解

    文件存放位置 全局配置: ${M2_HOME}/conf/settings.xml 用户配置: ${user.home}/.m2/settings.xml note:用户配置优先于全局配置.${use ...

  8. http消息与webservice

    别人的:在一台配置较低的PC上,同时开启服务端与客户端,10000条数据,使用基于http的消息逐条进行传递,从开始传递至全部接收并处理完毕,大概需要465秒的时间:而在同一台机器上,使用WebSer ...

  9. 深入详解JVM内存模型

    JVM内存结构 由上图可以清楚的看到JVM的内存空间分为3大部分: 堆内存 方法区 栈内存 其中栈内存可以再细分为java虚拟机栈和本地方法栈,堆内存可以划分为新生代和老年代,新生代中还可以再次划分为 ...

  10. java技术思维导图(转载)

      在网上看到有个人总结的java技术的东东,觉得很好,就保存下来了,码农还真是累啊,只有不断的学习才能有所提高,才能拿更多的RMB啊. java技术思维导图 服务端思维导图 前端思维导图