记录下app版本更新的代码实现
1.检测版本是否最新,根据服务器返回结果,进行强制更新或推荐更新提示
2.判断apk是否已有SD卡读写权限
Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.WRITE_EXTERNAL_STORAGE
3.在异步线程中进行apk文件下载操作
public class DownloadRunnable implements Runnable {
private DownApkListener downApkListener;
private TaskInfo info;//下载信息JavaBean
private boolean isStop;//是否暂停
/**
* 构造器
* @param info 任务信息
*/
public DownloadRunnable(TaskInfo info, DownApkListener downApkListener) {
this.downApkListener = downApkListener;
this.info = info;
}
/**
* 停止下载
*/
public void stop() {
isStop = true;
}
/**
* Runnable的run方法,进行文件下载
*/
@Override
public void run() {
HttpURLConnection conn;//http连接对象
BufferedInputStream bis;//缓冲输入流,从服务器获取
RandomAccessFile raf;//随机读写器,用于写入文件,实现断点续传
int len = 0;//每次读取的数组长度
byte[] buffer = new byte[1024 * 8];//流读写的缓冲区
try {
File dirFile = new File(info.getPath());
if (!dirFile.exists() || !dirFile.isDirectory()) {
dirFile.mkdirs();
}
//通过文件路径和文件名实例化File
File file = new File(info.getPath(),info.getName());
//实例化RandomAccessFile,rwd模式
raf = new RandomAccessFile(file, "rwd");
conn = (HttpURLConnection) new URL(info.getUrl()).openConnection();
conn.setConnectTimeout(120000);//连接超时时间
conn.setReadTimeout(120000);//读取超时时间
conn.setRequestMethod("GET");//请求类型为GET
if (info.getContentLen() == 0) {//如果文件长度为0,说明是新任务需要从头下载
//获取文件长度
info.setContentLen(Long.parseLong(conn.getHeaderField("content-length")));
} else {//否则设置请求属性,请求制定范围的文件流
conn.setRequestProperty("Range", "bytes=" + info.getCompletedLen() + "-" + info.getContentLen());
}
raf.seek(info.getCompletedLen());//移动RandomAccessFile写入位置,从上次完成的位置开始
conn.connect();//连接
bis = new BufferedInputStream(conn.getInputStream());//获取输入流并且包装为缓冲流
//从流读取字节数组到缓冲区
while (!isStop && -1 != (len = bis.read(buffer))) {
//把字节数组写入到文件
raf.write(buffer, 0, len);
//更新任务信息中的完成的文件长度属性
info.setCompletedLen(info.getCompletedLen() + len);
//更新进度
downApkListener.inProgress((int) (info.getCompletedLen()* 100.0f/info.getContentLen()));
}
if (len == -1) {//如果读取到文件末尾则下载完成
info.setComplete(true);
SPOperation.setApkTaskInfo(App.getAppContext(), CommonUtils.toJSONString(info));
downApkListener.onComplete(info.getPath()+File.separator+info.getName());
} else {//否则下载系手动停止
info.setComplete(false);
SPOperation.setApkTaskInfo(App.getAppContext(), CommonUtils.toJSONString(info));
LogUtils.i( "下载停止了");
}
raf.close();
} catch (IOException e) {
e.printStackTrace();
LogUtils.i(e);
}
}
//下载的进度回调,如果更新UI,需要在主线程中进行
public interface DownApkListener{
void inProgress(int progress);
void onComplete(String apkFullPath);
}
}
4.保存下载信息、暂停位置的bean类
private String name;//文件名
private String path;//文件路径
private String url;//链接
private long contentLen;//文件总长度
private boolean isComplete = false;//文件是否完成下载完成了
/**
* 迄今为止java虚拟机都是以32位作为原子操作,而long与double为64位,当某线程
* 将long/double类型变量读到寄存器时需要两次32位的操作,如果在第一次32位操作
* 时变量值改变,其结果会发生错误,简而言之,long/double是非线程安全的,volatile
* 关键字修饰的long/double的get/set方法具有原子性。
*/
private volatile long completedLen;//已完成长度
setXxx()
getXxx()
...
}
5.在onComplete()回调方法中,调起apk安装页面进行安装操作
先请求安装未知来源apk的权限,适配8.0+
/**
* 判断是否是8.0
* 8.0需要处理未知应用来源权限问题,否则直接安装
*/
public static void getInstallPermission(Activity activity,File apkFile) {
if (Build.VERSION.SDK_INT >= 26) {
boolean b = activity.getPackageManager().canRequestPackageInstalls();
if (b) {
install(activity,apkFile);
} else {
//请求安装未知应用来源的权限
AndPermission.with(activity)
.requestCode(Constant.INSTALL_PERMISS_CODE)
.permission(Manifest.permission.REQUEST_INSTALL_PACKAGES)
.callback(new PermissionListener() {
@Override
public void onSucceed(int requestCode, @NonNull List<String> grantPermissions) {
install(activity,apkFile);
}
@Override
public void onFailed(int requestCode, @NonNull List<String> deniedPermissions) {
ToastUitl.showShort("请允许安装应用");
//跳转到授予安装未知来源应用开关界面
Uri packageURI = Uri.parse("package:"+activity.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageURI);
activity.startActivityForResult(intent,Constant.INSTALL_PERMISS_CODE);
}
}).start();
}
} else {
install(activity,apkFile);
}
}
获取权限后,调起系统的安装界面
private static final String MIME_TYPE_APK = "application/vnd.android.package-archive";
private static final String APP_PROVIDE = "packagename.FileProvider";
/**
* 调起系统的安装界面
* @param activity
* @param apkFile
*/
public static void install(Activity activity,File apkFile) {
if (CommonUtils.isEmpty(apkFile) || !apkFile.exists()) {
return;
}
//提升读写权限,否则可能出现解析异常
SystemManager.setPermission(apkFile.getParent());
// 通过Intent安装APK文件
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
Uri uriForFile = FileProvider.getUriForFile(activity, APP_PROVIDE, apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uriForFile, MIME_TYPE_APK);
} else {
intent.setDataAndType(Uri.fromFile(apkFile), MIME_TYPE_APK);
}
activity.startActivity(intent);
}
其中APP_PROVIDE常量是自己在清单文件中配置的
<!--下载访问文件内容提供者-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="packagename.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/upgrade_file_paths"/>
</provider>
res\xml下upgrade_file_paths.xml文件是配置访问apk文件路径的适配7.0+,具体如何配置参考FileProvider 路径配置策略(7.0+)
本文参考允许安装未知来源权限(8.0+)