美文网首页Android开发Android 实战进阶Android版本更新
Android 实战-版本更新(okhttp3、service、

Android 实战-版本更新(okhttp3、service、

作者: BmobSnail | 来源:发表于2017-12-11 15:51 被阅读1533次

    转发请注明出处:http://www.jianshu.com/p/b669940c9f3e

    前言

    整理功能,把这块拿出来单独做个demo,好和大家分享交流一下。
    版本更新这个功能一般 app 都有实现,而用户获取新版本一般来源有两种:

    • 一种是各种应用市场的新版本提醒
    • 一种是打开app时拉取版本信息
    • (还要一种推送形式,热修复或打补丁包时用得多点)

    这两区别就在于,市场的不能强制更新、不够及时、粘度低、单调。

    摘要

    下面介绍这个章节,你将会学习或复习到一些技术:

    - dialog 实现 key 的重写,在弹窗后用户不能通过点击虚拟后退键关闭窗口
    - 忽略后不再提示,下个版本更新再弹窗
    - 自定义 service 来承载下载功能
    - okhttp3 下载文件到 sdcard,文件权限判断
    - 绑定服务,实现回调下载进度
    - 简易的 mvp 架构
    - 下载完毕自动安装
    
    <点分期>版本更新.png

    这个是我们公司的项目,有版本更新时的截图。当然,我们要实现的demo不会写这么复杂的ui。

    功能点(先把demo的最终效果给上看一眼)

    UpdateDemo.gif

    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文件夹再自定义一个文件,并在配置清单里面配置一下这个


    fileProvider.png AndroidMainfest.png

    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
    希望大家能从中学习到东西,不再困惑

    相关文章

      网友评论

      • 段浅浅儿:感谢楼主分享。
        顺便提出一个小问题,通过使用发现当下载成功后,会调起安装页面,杀死自己的进程,那么之后的通知取消和安装成功的逻辑不就执行不到了吗?
        BmobSnail:下载完成跳到安装界面,那原本的app基本监听不到安装过程了。
        也就是说能安装就足以完成了它的使命
      • 命若弦_T:朋友,现在安卓行情咋样,刚辞去老家国企工作,现在出去找安卓的好找么
        BmobSnail:@命若弦_T 这么根你说吧,为了保住Android这份工作,学会了python、h5、后台、小程序:joy::joy:
        命若弦_T:@Unbridledsmile 。。我也这样想的,现在学前端后台呢,哥们哪的
        Unbridledsmile:Android 没人要了
      • mapleeeeee:大神改为使用intentservice怎么样
        BmobSnail:@mapleeeeee service 有两种启动方式,startService和bindService,这里我们需要和它通讯交互,所以最好通过绑定启动这样子
      • 梦龙DRAG0N:支持:smirk:
        BmobSnail::clap: 谢谢

      本文标题:Android 实战-版本更新(okhttp3、service、

      本文链接:https://www.haomeiwen.com/subject/jzhdixtx.html