前言
整理功能,把这块拿出来单独做个demo,好和大家分享交流一下。
版本更新这个功能一般 app 都有实现,而用户获取新版本一般来源有两种:
一种是各种应用市场的新版本提醒
一种是打开app时拉取版本信息
(还要一种推送形式,热修复或打补丁包时用得多点)
这两区别就在于,市场的不能强制更新、不够及时、粘度低、单调。
摘要
下面介绍这个章节,你将会学习或复习到一些技术:
- dialog 实现 key 的重写,在弹窗后用户不能通过点击虚拟后退键关闭窗口
- 忽略后不再提示,下个版本更新再弹窗
- 自定义 service 来承载下载功能
- okhttp3 下载文件到 sdcard,文件权限判断
- 绑定服务,实现回调下载进度
- 简易的 mvp 架构
- 下载完毕自动安装
这个是我们公司的项目,有版本更新时的截图。当然,我们要实现的demo不会写这么复杂的ui。
功能点(先把demo的最终效果给上看一眼)
dialog
dialog.setCanceledOnTouchOutside() 触摸窗口边界以外是否关闭窗口,设置 false 即不关闭
dialog.setOnKeyListener() 设置KeyEvent的回调监听方法。如果事件分发到dialog的话,这个事件将被触发,一般是在窗口显示时,触碰屏幕的事件先分发到给它,但默认情况下不处理直接返回false,也就是继续分发给父级处理。如果只是拦截返回键就只需要这样写
mDialog.setOnKeyListener(new DialogInterface.OnKeyListener() { @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { return keyCode == KeyEvent.KEYCODE_BACK && mDialog != null && mDialog.isShowing(); } });
忽略
忽略本次版本更新,不再弹窗提示
下次有新版本时,再继续弹窗提醒
其实这样的逻辑很好理解,并没有什么特别的代码。比较坑的是,这里往往需要每次请求接口才能判断到你app是否已经是最新版本。
这里我并没有做网络请求,只是模拟一下得到的版本号,然后做一下常规的逻辑判断,在我们项目中,获取版本号只能通过请求接口来得到,也就是说每次启动请求更新的接口,也就显得非常浪费,我是建议把这个版本号的在你们的首页和其它接口信息一起返回,然后写入在 SharedPreferences。每次先判断与忽略的版本是否一样,一样则跳过,否则下次启动时请求更新接口
public void checkUpdate(String local) { //假设获取得到最新版本 //一般还要和忽略的版本做比对。。这里就不累赘了 String version = "2.0"; String ignore = SpUtils.getInstance().getString("ignore"); if (!ignore.equals(version) && !ignore.equals(local)) { view.showUpdate(version); } }
自定义service
这里需要和 service 通讯,我们自定义一个绑定的服务,需要重写几个比较关键的方法,分别是 onBind(返回和服务通讯的频道IBinder)、unbindService(解除绑定时销毁资源)、和自己写一个 Binder 用于通讯时返回可获取service对象。进行其它操作。
context.bindService(context,conn,flags)
context 上下文
conn(ServiceConnnetion),实现了这个接口之后会让你实现两个方法onServiceConnected(ComponentName, IBinder) 也就是通讯连通后返回我们将要操作的那个 IBinder 对象、onServiceDisconnected(ComponentName) 断开通讯
flags 服务绑定类型,它提供很多种类型,但常用的也就这里我我们用到的是 Service.BIND_AUTO_CREATE, 源码对它的描述大概意思是说,在你确保绑定此服务,就自动启动服务。(意思就是说,你bindService之后,传的不是这个参数,有可能你的服务就没反应咯)
通过获取这个对象就可以对 service 进行操作了。这个自定义service篇幅比较长,建议下载demo下来仔细阅读一番.
public class DownloadService extends Service { //定义notify的id,避免与其它的notification的处理冲突 private static final int NOTIFY_ID = 0; private static final String CHANNEL = "update"; private DownloadBinder binder = new DownloadBinder(); private NotificationManager mNotificationManager; private NotificationCompat.Builder mBuilder; private DownloadCallback callback; //定义个更新速率,避免更新通知栏过于频繁导致卡顿 private float rate = .0f; @Nullable @Override public IBinder onBind(Intent intent) { return binder; } @Override public void unbindService(ServiceConnection conn) { super.unbindService(conn); mNotificationManager.cancelAll(); mNotificationManager = null; mBuilder = null; } /** * 和activity通讯的binder */ public class DownloadBinder extends Binder{ public DownloadService getService(){ return DownloadService.this; } } /** * 创建通知栏 */ private void setNotification() { if (mNotificationManager == null) mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mBuilder = new NotificationCompat.Builder(this,CHANNEL); mBuilder.setContentTitle("开始下载") .setContentText("正在连接服务器") .setSmallIcon(R.mipmap.ic_launcher_round) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setOngoing(true) .setAutoCancel(true) .setWhen(System.currentTimeMillis()); mNotificationManager.notify(NOTIFY_ID, mBuilder.build()); } /** * 下载完成 */ private void complete(String msg) { if (mBuilder != null) { mBuilder.setContentTitle("新版本").setContentText(msg); Notification notification = mBuilder.build(); notification.flags = Notification.FLAG_AUTO_CANCEL; mNotificationManager.notify(NOTIFY_ID, notification); } stopSelf(); } /** * 开始下载apk */ public void downApk(String url,DownloadCallback callback) { this.callback = callback; if (TextUtils.isEmpty(url)) { complete("下载路径错误"); return; } setNotification(); handler.sendEmptyMessage(0); Request request = new Request.Builder().url(url).build(); new OkHttpClient().newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Message message = Message.obtain(); message.what = 1; message.obj = e.getMessage(); handler.sendMessage(message); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.body() == null) { Message message = Message.obtain(); message.what = 1; message.obj = "下载错误"; handler.sendMessage(message); return; } InputStream is = null; byte[] buff = new byte[2048]; int len; FileOutputStream fos = null; try { is = response.body().byteStream(); long total = response.body().contentLength(); File file = createFile(); fos = new FileOutputStream(file); long sum = 0; while ((len = is.read(buff)) != -1) { fos.write(buff,0,len); sum+=len; int progress = (int) (sum * 1.0f / total * 100); if (rate != progress) { Message message = Message.obtain(); message.what = 2; message.obj = progress; handler.sendMessage(message); rate = progress; } } fos.flush(); Message message = Message.obtain(); message.what = 3; message.obj = file.getAbsoluteFile(); handler.sendMessage(message); } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); if (fos != null) fos.close(); } catch (Exception e) { e.printStackTrace(); } } } }); } /** * 路径为根目录 * 创建文件名称为 updateDemo.apk */ private File createFile() { String root = Environment.getExternalStorageDirectory().getPath(); File file = new File(root,"updateDemo.apk"); if (file.exists()) file.delete(); try { file.createNewFile(); return file; } catch (IOException e) { e.printStackTrace(); } return null ; } /** * 把处理结果放回ui线程 */ private Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { switch (msg.what) { case 0: callback.onPrepare(); break; case 1: mNotificationManager.cancel(NOTIFY_ID); callback.onFail((String) msg.obj); stopSelf(); break; case 2:{ int progress = (int) msg.obj; callback.onProgress(progress); mBuilder.setContentTitle("正在下载:新版本...") .setContentText(String.format(Locale.CHINESE,"%d%%",progress)) .setProgress(100,progress,false) .setWhen(System.currentTimeMillis()); Notification notification = mBuilder.build(); notification.flags = Notification.FLAG_AUTO_CANCEL; mNotificationManager.notify(NOTIFY_ID,notification);} break; case 3:{ callback.onComplete((File) msg.obj); //app运行在界面,直接安装 //否则运行在后台则通知形式告知完成 if (onFront()) { mNotificationManager.cancel(NOTIFY_ID); } else { Intent intent = installIntent((String) msg.obj); PendingIntent pIntent = PendingIntent.getActivity(getApplicationContext() ,0, intent, PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent) .setContentTitle(getPackageName()) .setContentText("下载完成,点击安装") .setProgress(0,0,false) .setDefaults(Notification.DEFAULT_ALL); Notification notification = mBuilder.build(); notification.flags = Notification.FLAG_AUTO_CANCEL; mNotificationManager.notify(NOTIFY_ID,notification); } stopSelf();} break; } return false; } }); /** * 是否运行在用户前面 */ private boolean onFront() { ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses(); if (appProcesses == null || appProcesses.isEmpty()) return false; for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { if (appProcess.processName.equals(getPackageName()) && appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { return true; } } return false; } /** * 安装 * 7.0 以上记得配置 fileProvider */ private Intent installIntent(String path){ try { File file = new File(path); String authority = getApplicationContext().getPackageName() + ".fileProvider"; Uri fileUri = FileProvider.getUriForFile(getApplicationContext(), authority, file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(fileUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); } return intent; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 销毁时清空一下对notify对象的持有 */ @Override public void onDestroy() { mNotificationManager = null; super.onDestroy(); } /** * 定义一下回调方法 */ public interface DownloadCallback{ void onPrepare(); void onProgress(int progress); void onComplete(File file); void onFail(String msg); } }
okhttp3下载文件
看通透事情的本质,你就可以为所欲为了。怎么发起一个 okhttp3 最简单的请求,看下面!简洁明了吧,这里抽离出来分析一下,最主要还是大家的业务、框架、需求都不一样,所以节省时间看明白写入逻辑就好了,这样移植到自己项目的时候不至于无从下手。明白之后再结合比较流行常用的如 Retrofit、Volley之类的插入这段就好了。避免引入过多的第三方库而导致编译速度变慢,项目臃肿嘛。
我们来看回上面的代码,看到 downApk 方法,我是先判断路径是否为空,为空就在通知栏提示用户下载路径错误了,这样感觉比较友好。判断后就创建一个 request 并执行这个请求。很容易就理解了,我们要下载apk,只需要一个 url 就足够了是吧(这个url一般在检测版本更新接口时后台返回)。然后第一步就配置好了,接下来是处理怎么把文件流写出到 sdcard。
写入:是指读取文件射进你app内(InputStream InputStreamReader FileInputStream BufferedInputStream)
写出:是指你app很无赖的拉出到sdcard(OutputStream OutputStreamWriter FileOutputStream BufferedOutputStream)
仅此送给一直对 input、ouput 记忆混乱的同学
Request request = new Request.Builder().url(url).build(); new OkHttpClient().newCall(request).enqueue(new Callback() {});
写出文件
InputStream is = null; byte[] buff = new byte[2048]; int len; FileOutputStream fos = null; try { is = response.body().byteStream(); //读取网络文件流 long total = response.body().contentLength(); //获取文件流的总字节数 File file = createFile(); //自己的createFile() 在指定路径创建一个空文件并返回 fos = new FileOutputStream(file); //消化了上厕所准备了 long sum = 0; while ((len = is.read(buff)) != -1) { //嘣~嘣~一点一点的往 sdcard &#$%@$%#%$ fos.write(buff,0,len); sum+=len; int progress = (int) (sum * 1.0f / total * 100); if (rate != progress) { //用handler回调通知下载进度的 rate = progress; } } fos.flush(); //用handler回调通知下载完成 } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); if (fos != null) fos.close(); } catch (Exception e) { e.printStackTrace(); } }
文件下载回调
在上面的okhttp下载处理中,我注释标注了回调的位置,因为下载线程不再UI线程中,大家需要通过handler把数据先放回我们能操作UI的线程中再返回会比较合理,在外面实现了该回调的时候就可以直接处理数据。
private Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { switch (msg.what) { case 0://下载操作之前的预备操作,如检测网络是否wifi callback.onPrepare(); break; case 1://下载失败,清空通知栏,并销毁服务自己 mNotificationManager.cancel(NOTIFY_ID); callback.onFail((String) msg.obj); stopSelf(); break; case 2:{//回显通知栏的实时进度 int progress = (int) msg.obj; callback.onProgress(progress); mBuilder.setContentTitle("正在下载:新版本...") .setContentText(String.format(Locale.CHINESE,"%d%%",progress)) .setProgress(100,progress,false) .setWhen(System.currentTimeMillis()); Notification notification = mBuilder.build(); notification.flags = Notification.FLAG_AUTO_CANCEL; mNotificationManager.notify(NOTIFY_ID,notification);} break; case 3:{//下载成功,用户在界面则直接安装,否则叮一声通知栏提醒,点击通知栏跳转到安装界面 callback.onComplete((File) msg.obj); if (onFront()) { mNotificationManager.cancel(NOTIFY_ID); } else { Intent intent = installIntent((String) msg.obj); PendingIntent pIntent = PendingIntent.getActivity(getApplicationContext() ,0, intent, PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(pIntent) .setContentTitle(getPackageName()) .setContentText("下载完成,点击安装") .setProgress(0,0,false) .setDefaults(Notification.DEFAULT_ALL); Notification notification = mBuilder.build(); notification.flags = Notification.FLAG_AUTO_CANCEL; mNotificationManager.notify(NOTIFY_ID,notification); } stopSelf();} break; } return false; } });
自动安装
android 随着版本迭代的速度越来越快,有一些api已经被遗弃了甚至不存在了。7.0 的文件权限变得尤为严格,所以之前的一些代码在高一点的系统可能导致崩溃,比如下面的,如果不做版本判断,在7.0的手机就会抛出FileUriExposedException异常,说app不能访问你的app以外的资源。官方文档建议的做法,是用FileProvider来实现文件共享。也就是说在你项目的src/res新建个xml文件夹再自定义一个文件,并在配置清单里面配置一下这个
file_paths.xml
<?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external" path=""/> </paths>
安装apk
try { String authority = getApplicationContext().getPackageName() + ".fileProvider"; Uri fileUri = FileProvider.getUriForFile(this, authority, file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //7.0以上需要添加临时读取权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(fileUri, "application/vnd.android.package-archive"); } else { Uri uri = Uri.fromFile(file); intent.setDataAndType(uri, "application/vnd.android.package-archive"); } startActivity(intent); //弹出安装窗口把原程序关闭。 //避免安装完毕点击打开时没反应 killProcess(android.os.Process.myPid()); } catch (Exception e) { e.printStackTrace(); }
已把 Demo 放在github
希望大家能从中学习到东西,不再困惑