×

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

96
BmobSnail
2017.12.11 15:51* 字数 1596

转发请注明出处: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
希望大家能从中学习到东西,不再困惑

技术博文
Web note ad 1