应用更新是移动应用开发中的一个常见功能。主要分为三步:
(1)检测应用新版本
检测应用新版本,一般是进入应用时调取服务器接口,获取最新应用版 本信息,和当前应用版本信息进行比较,如果当前应用不是最新版本,则下载最新应用并自动跳转至安装界面。
(2)下载并安装
下载应用文件(APK文件),系统自身提供了DownloadManager可以实现文件的下载。网上很多应用更新的文章、博客也都是使用DownloadManager来下载APK文件的,但是经过实际测试,DownloadManager存在兼容性问题,在有些机型上使用DownloadManager下载APK文件后,安装时出现解析包错误;还有些机型上,使用DownloadManager无法下载文件。所以为了避免这些兼容问题,我没有采用DownloadManager下载文件,而是通过OKHttp来实现文件的下载。
(3)调起应用安装界面
在Android N之前,可以通过如下代码来调起应用安装界面
/*
* mSavePath是下载的APK文件保存的路径
*/
File saveFile = new File(mSavePath);
Intent install = new Intent(Intent.ACTION_VIEW);
install.setDataAndType(Uri.fromFile(saveFile),
"application/vnd.android.package-archive");
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(install);
但是从Android N开始,为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访,传递软件包网域外的 file:// URI 可能给接收器留下无法访问的路径。因此,尝试传递 file:// URI 会触发 FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
使用FileProvider的步骤:
第一步:
在AndroidManifest.xml清单文件中注册provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.maqing.appupdatedemo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
"android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"
/>
注意:
exported:要求必须为false,为true则会报安全异常。
grantUriPermissions:true,表示授予 URI 临时访问权限。
authorities 组件标识,按照江湖规矩,都以包名开头,避免和其它应用发生冲突。
第二步:
上面配置文件中 android:resource=”@xml/file_paths” 指的是当前组件引用 res/xml/file_paths.xml 这个文件。
我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:
<paths>
<external-path path="" name="download"/>
paths>
< external-path /> 代表的根目录:
Environment.getExternalStorageDirectory()
如果是< files-path /> 代表的根目录: Context.getFilesDir()
如果是< cache-path /> 代表的根目录: getCacheDir()
上述代码中path=””,是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。
如果你将path设为path=”pictures”,那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。
第三步:
使用FileProvider
调起应用安装的界面如下:
/*
* mSavePath是下载的APK文件保存的路径
*/
File saveFile = new File(mSavePath);
Intent install = new Intent(Intent.ACTION_VIEW);
/*
*BuildConfig.APPLICATION_ID + ".fileprovider"是
*在AndroidManifest中的注册的Provider的android:authorities值
*/
Uri apkUri = FileProvider.getUriForFile(getApplicationContext(),
BuildConfig.APPLICATION_ID + ".fileprovider", saveFile);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
/*
*添加这一句表示对目标应用临时授权该Uri所代表的文件
*/ install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(install);
上述代码中主要有两处改变:
将之前Uri改成了有FileProvider创建一个content类型的Uri。
添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。
所以为了适配Android N,调起应用安装界面,需要对Android版本做判断
File saveFile = new File(mSavePath);
Intent install = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) { //判读版本是否在7.0以上
Uri apkUri = FileProvider.getUriForFile(getApplicationContext(),
BuildConfig.APPLICATION_ID + ".fileprovider", saveFile);
install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
}
else {
install.setDataAndType(Uri.fromFile(saveFile), "application/vnd.android.package-archive");
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(install);
下载APK文件,以及下载完成后调起应用安装界面都是放在Servcie中。
Service代码:
public class AppUpdateService extends Service {
private NotificationManager mNotificationManager;
private Notification mNotification;
/**
* 保存的路径
*/
private String mSavePath;
/**
* 下载的Url
*/
private String mDownloadUrl;
private int mOldProgress = 0;
private String TAG = "AppUpdateService";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
mSavePath = intent.getStringExtra("save_path");
mDownloadUrl = intent.getStringExtra("download_url");
Log.e(TAG, mSavePath + "," + mDownloadUrl);
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
File saveFile = new File(mSavePath);
HttpRequest.download(mDownloadUrl, saveFile, new FileDownloadCallback() {
//开始下载
@Override
public void onStart() {
super.onStart();
}
//下载进度
@Override
public void onProgress(final int progress, long networkSpeed) {
super.onProgress(progress, networkSpeed);
//这里的条件判断是为了避免Notification的频繁notify,因为Notification的频繁notify会导致界面卡顿和卡死(无响应)
if (mOldProgress ==0 || (mOldProgress>0&&(progress - mOldProgress) > 10)||progress==100) {
mOldProgress=progress;
notifyUser(progress);
}
}
//下载失败
@Override
public void onFailure() {
super.onFailure();
Toast.makeText(getBaseContext(), "下载失败", Toast.LENGTH_SHORT).show();
}
//下载完成(下载成功)
@Override
public void onDone() {
super.onDone();
Toast.makeText(getBaseContext(), "下载成功", Toast.LENGTH_SHORT).show();
//关闭Service
stopSelf();
}
});
}
return super.onStartCommand(intent, flags, startId);
}
/*
*通知栏通知
*/
private void notifyUser(int progress) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.logo)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.logo))
.setContentTitle(getString(R.string.app_name));
if (progress > 0 && progress <= 100) {
builder.setProgress(100, progress, false);
} else {
builder.setProgress(0, 0, false);
}
builder.setAutoCancel(true);
builder.setWhen(System.currentTimeMillis());
// builder.setTicker(result);
builder.setContentIntent(progress >= 100 ? this.getContentIntent() :
PendingIntent.getActivity(this, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT));
mNotification = builder.build();
mNotificationManager.notify(0, mNotification);
}
/**
* 进入安装
*
* @return
*/
private PendingIntent getContentIntent() {
File saveFile = new File(mSavePath);
Intent install = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) { //判读版本是否在7.0以上
Uri apkUri = FileProvider.getUriForFile(getApplicationContext(), BuildConfig.APPLICATION_ID + ".fileprovider", saveFile);//在AndroidManifest中的android:authorities值
install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
install.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
install.setDataAndType(Uri.fromFile(saveFile), "application/vnd.android.package-archive");
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, install, PendingIntent.FLAG_UPDATE_CURRENT);
startActivity(install);
return pendingIntent;
}
/**
* @param context
* @param savePath 保存到本地的路径
* @param downloadUrl 下载的Url
*/
public static void start(Context context, String savePath, String downloadUrl) {
Intent intent = new Intent(context, AppUpdateService.class);
intent.putExtra("save_path", savePath);
intent.putExtra("download_url", downloadUrl);
context.startService(intent);
}
}
注意这里下载APK文件,是使用的一个第三方的封装好的文件下载OkHttpFinal,
基于OkHttp的,使用的话需要依赖:
compile ‘cn.finalteam:okhttpfinal:2.0.7’
OkHttpFinal的GitHub地址:
https://github.com/pengjianbo/OkHttpFinal
使用OKHttpFinal要进行初始化
OkHttpFinalConfiguration.Builder builder = new OkHttpFinalConfiguration.Builder();
OkHttpFinal.getInstance().init(builder.build());
使用,检测到有新版本时,通过如下代码开启Service,下载APK文件,调起应用安装界面
AppUpdateService.start(mContext,mSavePath,mDownloadUrl);
使用之前
在AndroidManifest.xml中注册该AppUpdateService,
<service android:name=".AppUpdateService"/>
并且不要忘了添加相关权限(Android 6.0及以上需要动态获取权限):
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
源码:
https://github.com/maqing-programmer/AppUpdateDemo