基本上什么App都会有的功能,在此列举一个之前项目使用的,请求是Rxjava+Retrofit,显示进度是通知栏进度条。
1.判断是否需要更新部分
2.请求下载apk
3.进度条更新
4.下载后自动安装
5.其他问题
测试demo请参考QingFrame中的相关测试模块:
https://github.com/UncleQing/QingFrame
1.判断是否需要更新部分
这部分很简单,基本就是请求自己服务器相关接口,服务器根据我们上传的版本号返回信息,如果没有返回信息则不需更新,如果有再判定是否需要强制更新(进入首页自动获取),只要需要更新则显示一个dialog,显示出更新细节以及一个Button,点击进行下载apk
下面是请求后的回调,如果beans不为空则需要更新,根据beans中带有url请求下载
public void onLoadingVersionSucceed(final List<VersionBean> beans) { if (beans == null || beans.size() == 0) { MyToast.showToast(getContext(), "当前已是最新版本"); } else { //更新询问对话框 DialogFragmentHelper.showUpdateDialog(getFragmentManager(), beans.get(0).getCopywriting() , true, new View.OnClickListener() { @Override public void onClick(View view) { //下载apk mPresenter.downLoadApk(beans.get(0).getDownload_url()); } }); } }
"copywriting": "更新内容:...", "forced_update": "是否强制更新 1 是,2 否", "download_url": "下载地址"
2.请求下载apk
这部分根据使用的网络框架不同实现不同,只介绍Rxjava+Retrofit这种的
Presenter:
public class UpdatePresenter { //路径:data/包名/file/,保证app卸载后不会残留垃圾 private final static String DOWNLOAD_DIR = AppUtils.getApp().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + File.separator; //下载后的apk文件名字,保证后缀.apk就行 private static String APP_FILE = "qingFrame.apk"; //相关的activity private UpdateActivity mView; UpdatePresenter(IBaseView view) { mView = (UpdateActivity) view; } /** * 根据url下载apk * * @param url */ public void downLoadApk(String url) { if (AppConfig.isDownLoad) { mView.showToast("已有相同任务"); return; } RetrofitApiService.getInstance().downloadApk(url).subscribeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) .subscribe(new Observer<ResponseBody>() { @Override public void onSubscribe(Disposable d) { mView.showToast("已添加到下载任务"); AppConfig.isDownLoad = true; } @Override public void onNext(ResponseBody responseBody) { File file = FileUtils.get().writeFile(responseBody.byteStream(), DOWNLOAD_DIR, APP_FILE); mView.onDownloadSucceed(file); AppConfig.isDownLoad = false; } @Override public void onError(Throwable e) { if (!TextUtils.isEmpty(e.getMessage())) { mView.showToast(e.getMessage()); } AppConfig.isDownLoad = false; } @Override public void onComplete() { AppConfig.isDownLoad = false; } }); } }
Retrofit调用接口
RetrofitApiService
/** * 下载apk * * @return */ public Observable<ResponseBody> downloadApk(String url) { //传入头信息"upgrade"是为了触发自定义的下载拦截器 return mApiService.download(url, "upgrade"); }
IApiService
/** * 下载apk, 不使用缓存,因为配置OKhttp配置缓存了,如果此部分依然使用缓存则只会下载一次 * @param url * @param head * @return */ @Streaming @GET @Headers({"Accept-Encoding:identity","Cache-Control:no-store"}) Observable<ResponseBody> download(@Url String url, @Header("Upgrade") String head);
请求方式Get以流形式获取,获取成功后在presenter的onNext方法写入到相关文件
writeFile
/** * 将输入流写入文件 * * @param inputString * @param fileDir * @param fileName */ public File writeFile(InputStream inputString, String fileDir, String fileName) { File dir = new File(fileDir); if (!dir.exists()) { dir.mkdirs(); } File file = new File(fileDir, fileName); if (file.exists()) { file.delete(); } FileOutputStream fos = null; try { fos = new FileOutputStream(file); byte[] b = new byte[1024]; int len; while ((len = inputString.read(b)) != -1) { fos.write(b, 0, len); } inputString.close(); fos.close(); } catch (Exception e) { e.printStackTrace(); LogUtils.e("Exception: " + e.getMessage()); } return file; }
3.进度条更新
上一步完成后就可以获取到apk文件了,但是下载过程UI没有任何显示,因此需要在下载过程中显示在通知栏上。
首先,配置OKHttp时需要配置一个自定义的下载拦截器
/** * 下载拦截器,用于进度条显示 */ public class DownloadInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); String value = request.header("Upgrade"); if (!TextUtils.isEmpty(value) || "upgrade".equals(value)) { //只有是下载的时候会包含这个Upgrade头信息 return response.newBuilder() .body(new ProgressResponseBody(response.body())) .build(); } return response; } }
然后在第二步Retrofit接口请求要带入这个头信息,这样下载的时候就可以出发我们自定义的body了
ProgressResponseBody
public class ProgressResponseBody extends ResponseBody { private final ResponseBody responseBody; private BufferedSource bufferedSource; public ProgressResponseBody(ResponseBody responseBody) { this.responseBody = responseBody; } @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } private Source source(Source source) { return new ForwardingSource(source) { long totalBytesRead = 0L; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); totalBytesRead += bytesRead != -1 ? bytesRead : 0; Intent mIntent = new Intent(AppConfig.DOWNLOAD_PROGRESS); mIntent.putExtra("totalBytesRead", totalBytesRead); mIntent.putExtra("total", responseBody.contentLength()); //将下载进度通过广播发送出去 AppUtils.getApp().sendBroadcast(mIntent); return bytesRead; } }; } }
发出了广播,在相关activity注册广播接收即可
UpdateActivity
private void registerReceiver() { //下载进度条 if (null == mReceiver) { mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (AppConfig.DOWNLOAD_PROGRESS.equals(action)) { double total = (double) intent.getLongExtra("total", 0); double current = (double) intent.getLongExtra("totalBytesRead", 0); if (!isCreat) { builder = UIUtils.creatNotification(notificationManager); isCreat = true; pro = 0; } pro = (int) (current / total * 100); if (pro < 100) { builder.setContentText(pro + "%"); builder.setProgress(100, pro, false); notificationManager.notify(UIUtils.notifyID, builder.build()); } else { UIUtils.cancleNotification(notificationManager); isCreat = false; } } } }; IntentFilter filter = new IntentFilter(); filter.addAction(AppConfig.DOWNLOAD_PROGRESS); AppUtils.getApp().registerReceiver(mReceiver, filter); } }
UIUtils
/** * 下载apk通知栏 * @param notificationManager * @return */ public static final int notifyID = 99; public static final String CHANNEL_ID = "111"; public static Notification.Builder creatNotification(NotificationManager notificationManager) { if (notificationManager == null) { notificationManager = (NotificationManager) AppUtils.getApp().getSystemService(Context.NOTIFICATION_SERVICE); } Notification.Builder builder = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //8.0新增通知渠道 CharSequence name = "QingFrame"; int importance = NotificationManager.IMPORTANCE_LOW; //优先级 NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance); mChannel.enableLights(false); //闪灯开关 mChannel.enableVibration(false); //振动开关 mChannel.setShowBadge(false); //通知圆点开关 notificationManager.createNotificationChannel(mChannel); builder = new Notification.Builder(AppUtils.getApp(), CHANNEL_ID); } else { builder = new Notification.Builder(AppUtils.getApp()); } builder.setSmallIcon(R.drawable.ic_app_ntfc) .setLargeIcon(BitmapFactory.decodeResource(AppUtils.getApp().getResources(), R.mipmap.share_)) .setContentText("0%") .setContentTitle("青结构") .setProgress(100, 0, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //小图标背景 builder.setColor(AppUtils.getApp().getResources().getColor(R.color.colorPrimary)); } notificationManager.notify(notifyID, builder.build()); return builder; } public static void cancleNotification(NotificationManager notificationManager) { if (notificationManager == null) { notificationManager = (NotificationManager) AppUtils.getApp().getSystemService(Context.NOTIFICATION_SERVICE); } notificationManager.cancel(notifyID); }
值得注意的是,在8.0Notification行为变更,需要有渠道相关信息,另外如果notification的小图标是需要一个没有颜色切图。
4.下载后自动安装
上面设置完后就可以完整下载一个apk到本地了,接下来是自动安装。
CommonUtil
//自动安装apk public static void installApk(Context context, File apkPath) { //提升文件读写权限 String command = "chmod " + "777" + " " + apkPath.getPath(); Runtime runtime = Runtime.getRuntime(); try { runtime.exec(command); } catch (IOException e) { e.printStackTrace(); } //安装跳转 Intent intent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //这块不要写错了,一定要是你自己Manifest注册的fileprovider Uri apkUri = FileProvider.getUriForFile(context, "com.zidian.qingframe.fileprovider", apkPath); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(apkPath), "application/vnd.android.package-archive"); } context.startActivity(intent); }
设置完成去应用市场找个apk链接试下吧,为了排除非代码原因的安卓失败,建议链接先从浏览器下载后手动安装,确认无误再使用代码测试
5.其他问题
再次重申几个问题
1.下载安装apk需要的权限,sd卡读取和网络请求就不说了,还需要一个
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
2.解析apk的contentUri需要配置fileprovider,不要写错了
参考:Android App的更新
3.Retrofit接口如果按我的这个记得传个头信息"upgrade",另外如果OKhttp也是配置缓存的话需要去掉缓存请求
4.Notification中小图标如果是有颜色的会显示白色方块,所以需要美工单独切个没有颜色的图,然后我们自己配置上颜色
以上贴的代码在文首链接那个demo都有,可能有些叙述的不彻底,想参考的朋友请直接去demo中看吧。另外关于强制更新再说一下,一般的都是进入首页获取更新信息,如果需要强制更新,则弹出的这个更新对话框不可关闭,即用户必须点击“立即更新”的按钮,否则不可用,点击之后分两种,一种可以让用户继续使用,直到下载完成后跳转到安装界面;另一种是将更新进度也是dialog显示,并且也是不可关闭的。