美文网首页Android移动开发Android技术知识
Android应用更新-自动检测版本及自动升级

Android应用更新-自动检测版本及自动升级

作者: 流水潺湲 | 来源:发表于2017-03-17 13:32 被阅读1133次

    步骤:

    • 1.检测当前版本的信息AndroidManifest.xml-->manifest-->[Android]

    • 2.从服务器获取版本号(版本号存在于xml文件中)并与当前检测到的版本进行匹配,如果不匹配,提示用户进行升级,如果匹配则进入程序主界面。(demo中假设需要更新)

    • 3.当提示用户进行版本升级时,如果用户点击了“更新”,系统将自动从服务器上下载安装包并进行自动升级,如果点击取消将进入程序主界面。

    效果图如下:

    更新
    下载1 下载2 安装

    下面介绍一下代码的实现:

    • 1.获取应用的当前版本号,我是封装了一个工具类来获取
     // 获取本版本号,是否更新
            int vision = Tools.getVersion(this);
    
    

    获取当前版本号工具类:

    
    public class Tools {
        /**
         * 检查是否存在SDCard
         *
         * @return
         */
        public static boolean hasSdcard() {
            String state = Environment.getExternalStorageState();
            if (state.equals(Environment.MEDIA_MOUNTED)) {
                return true;
            } else {
                return false;
            }
        }
    
        /**
         * 2 * 获取版本号 3 * @return 当前应用的版本号 4
         */
        public static int getVersion(Context context) {
            try {
                PackageManager manager = context.getPackageManager();
                PackageInfo info = manager.getPackageInfo(context.getPackageName(),
                        0);
                String version = info.versionName;
                int versioncode = info.versionCode;
                return versioncode;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return 0;
        }
       
    }
    
    • 2.获取服务器版本号,是否要更新(此处就是简单的网络请求拿到需要的数据即可,我是写了固定值)
     // 获取更新版本号
        private void getVersion(final int vision) {
    //         {"data":{"content":"其他bug修复。","id":"2","api_key":"android",
    //         // "version":"2.1"},"msg":"获取成功","status":1}
            String data = "";
            //网络请求获取当前版本号和下载链接
            //实际操作是从服务器获取
            //demo写死了
    
            String newversion = "2.1";//更新新的版本号
            String content = "\n" +
                    "就不告诉你我们更新了什么-。-\n" +
                    "\n" +
                    "----------万能的分割线-----------\n" +
                    "\n" +
                    "(ㄒoㄒ) 被老板打了一顿,还是来告诉你吧:\n" +
    
                    "1.下架商品误买了?恩。。。我搞了点小动作就不会出现了\n" +
                    "2.侧边栏、弹框优化 —— 这个你自己去探索吧,总得留点悬念嘛-。-\n";//更新内容
            String url = "http://openbox.mobilem.360.cn/index/d/sid/3429345";//安装包下载地址
    
            double newversioncode = Double
                    .parseDouble(newversion);
            int cc = (int) (newversioncode);
    
            System.out.println(newversion + "v" + vision + ",,"
                    + cc);
            if (cc != vision) {
                if (vision < cc) {
                    System.out.println(newversion + "v"
                            + vision);
                    // 版本号不同
                    ShowDialog(vision, newversion, content, url);
                }
            }
        }
    
    • 3.接下来就是下载文件了
      (1) 显示下载
      此处用的是自定义按钮:
     /**
         * 升级系统
         *
         * @param content
         * @param url
         */
        private void ShowDialog(int vision, String newversion, String content,
                                final String url) {
            final MaterialDialog dialog = new MaterialDialog(this);
            dialog.content(content).btnText("取消", "更新").title("版本更新 ")
                    .titleTextSize(15f).show();
            dialog.setCanceledOnTouchOutside(false);
            dialog.setOnBtnClickL(new OnBtnClickL() {// left btn click listener
                @Override
                public void onBtnClick() {
                    dialog.dismiss();
                }
            }, new OnBtnClickL() {// right btn click listener
    
                @Override
                public void onBtnClick() {
                    dialog.dismiss();
                    // pBar = new ProgressDialog(MainActivity.this,
                    // R.style.dialog);
                    pBar = new CommonProgressDialog(MainActivity.this);
                    pBar.setCanceledOnTouchOutside(false);
                    pBar.setTitle("正在下载");
                    pBar.setCustomTitle(LayoutInflater.from(
                            MainActivity.this).inflate(
                            R.layout.title_dialog, null));
                    pBar.setMessage("正在下载");
                    pBar.setIndeterminate(true);
                    pBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
                    pBar.setCancelable(true);
                    // downFile(URLData.DOWNLOAD_URL);
                    final DownloadTask downloadTask = new DownloadTask(
                            MainActivity.this);
                    downloadTask.execute(url);
                    pBar.setOnCancelListener(new DialogInterface.OnCancelListener() {
                        @Override
                        public void onCancel(DialogInterface dialog) {
                            downloadTask.cancel(true);
                        }
                    });
                }
            });
        }
    
    

    原生的按钮:

     new android.app.AlertDialog.Builder(this)
                    .setTitle("版本更新")
                    .setMessage(content)
                    .setPositiveButton("更新", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            pBar = new CommonProgressDialog(MainActivity.this);
                            pBar.setCanceledOnTouchOutside(false);
                            pBar.setTitle("正在下载");
                            pBar.setCustomTitle(LayoutInflater.from(
                                    MainActivity.this).inflate(
                                    R.layout.title_dialog, null));
                            pBar.setMessage("正在下载");
                            pBar.setIndeterminate(true);
                            pBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
                            pBar.setCancelable(true);
                            // downFile(URLData.DOWNLOAD_URL);
                            final DownloadTask downloadTask = new DownloadTask(
                                    MainActivity.this);
                            downloadTask.execute(url);
                            pBar.setOnCancelListener(new DialogInterface.OnCancelListener() {
                                @Override
                                public void onCancel(DialogInterface dialog) {
                                    downloadTask.cancel(true);
                                }
                            });
                        }
                    })
                    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                        }
                    })
                    .show();
    

    (2)通过异步任务实现进度++

    
        /**
         * 下载应用
         *
         * @author Administrator
         */
        class DownloadTask extends AsyncTask<String, Integer, String> {
    
            private Context context;
            private PowerManager.WakeLock mWakeLock;
    
            public DownloadTask(Context context) {
                this.context = context;
            }
    
            @Override
            protected String doInBackground(String... sUrl) {
                InputStream input = null;
                OutputStream output = null;
                HttpURLConnection connection = null;
                File file = null;
                try {
                    URL url = new URL(sUrl[0]);
                    connection = (HttpURLConnection) url.openConnection();
                    connection.connect();
                    // expect HTTP 200 OK, so we don't mistakenly save error
                    // report
                    // instead of the file
                    if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                        return "Server returned HTTP "
                                + connection.getResponseCode() + " "
                                + connection.getResponseMessage();
                    }
                    // this will be useful to display download percentage
                    // might be -1: server did not report the length
                    int fileLength = connection.getContentLength();
                    if (Environment.getExternalStorageState().equals(
                            Environment.MEDIA_MOUNTED)) {
                        file = new File(Environment.getExternalStorageDirectory(),
                                DOWNLOAD_NAME);
    
                        if (!file.exists()) {
                            // 判断父文件夹是否存在
                            if (!file.getParentFile().exists()) {
                                file.getParentFile().mkdirs();
                            }
                        }
    
                    } else {
                        Toast.makeText(MainActivity.this, "sd卡未挂载",
                                Toast.LENGTH_LONG).show();
                    }
                    input = connection.getInputStream();
                    output = new FileOutputStream(file);
                    byte data[] = new byte[4096];
                    long total = 0;
                    int count;
                    while ((count = input.read(data)) != -1) {
                        // allow canceling with back button
                        if (isCancelled()) {
                            input.close();
                            return null;
                        }
                        total += count;
                        // publishing the progress....
                        if (fileLength > 0) // only if total length is known
                            publishProgress((int) (total * 100 / fileLength));
                        output.write(data, 0, count);
    
                    }
                } catch (Exception e) {
                    System.out.println(e.toString());
                    return e.toString();
    
                } finally {
                    try {
                        if (output != null)
                            output.close();
                        if (input != null)
                            input.close();
                    } catch (IOException ignored) {
                    }
                    if (connection != null)
                        connection.disconnect();
                }
                return null;
            }
    
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                // take CPU lock to prevent CPU from going off if the user
                // presses the power button during download
                PowerManager pm = (PowerManager) context
                        .getSystemService(Context.POWER_SERVICE);
                mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                        getClass().getName());
                mWakeLock.acquire();
                pBar.show();
            }
    
            @Override
            protected void onProgressUpdate(Integer... progress) {
                super.onProgressUpdate(progress);
                // if we get here, length is known, now set indeterminate to false
                pBar.setIndeterminate(false);
                pBar.setMax(100);
                pBar.setProgress(progress[0]);
            }
    
            @Override
            protected void onPostExecute(String result) {
                mWakeLock.release();
                pBar.dismiss();
                if (result != null) {
    
    //                // 申请多个权限。大神的界面
    //                AndPermission.with(MainActivity.this)
    //                        .requestCode(REQUEST_CODE_PERMISSION_OTHER)
    //                        .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
    //                        // rationale作用是:用户拒绝一次权限,再次申请时先征求用户同意,再打开授权对话框,避免用户勾选不再提示。
    //                        .rationale(new RationaleListener() {
    //                                       @Override
    //                                       public void showRequestPermissionRationale(int requestCode, Rationale rationale) {
    //                                           // 这里的对话框可以自定义,只要调用rationale.resume()就可以继续申请。
    //                                           AndPermission.rationaleDialog(MainActivity.this, rationale).show();
    //                                       }
    //                                   }
    //                        )
    //                        .send();
                    // 申请多个权限。
                    AndPermission.with(MainActivity.this)
                            .requestCode(REQUEST_CODE_PERMISSION_SD)
                            .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
                            // rationale作用是:用户拒绝一次权限,再次申请时先征求用户同意,再打开授权对话框,避免用户勾选不再提示。
                            .rationale(rationaleListener
                            )
                            .send();
    
    
                    Toast.makeText(context, "您未打开SD卡权限" + result, Toast.LENGTH_LONG).show();
                } else {
                    // Toast.makeText(context, "File downloaded",
                    // Toast.LENGTH_SHORT)
                    // .show();
                    update();
                }
    
            }
        }
    

    此处下载apk文件,需要获取SD的读写权限(用的是严大的权限库)
    权限库GitHub:https://github.com/yanzhenjie/AndPermission

      private static final int REQUEST_CODE_PERMISSION_SD = 101;
    
        private static final int REQUEST_CODE_SETTING = 300;
        private RationaleListener rationaleListener = new RationaleListener() {
            @Override
            public void showRequestPermissionRationale(int requestCode, final Rationale rationale) {
                // 这里使用自定义对话框,如果不想自定义,用AndPermission默认对话框:
                // AndPermission.rationaleDialog(Context, Rationale).show();
    
                // 自定义对话框。
                AlertDialog.build(MainActivity.this)
                        .setTitle(R.string.title_dialog)
                        .setMessage(R.string.message_permission_rationale)
                        .setPositiveButton(R.string.btn_dialog_yes_permission, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.cancel();
                                rationale.resume();
                            }
                        })
    
                        .setNegativeButton(R.string.btn_dialog_no_permission, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.cancel();
                                rationale.cancel();
                            }
                        })
                        .show();
            }
        };
        //----------------------------------SD权限----------------------------------//
    
    
        @PermissionYes(REQUEST_CODE_PERMISSION_SD)
        private void getMultiYes(List<String> grantedPermissions) {
            Toast.makeText(this, R.string.message_post_succeed, Toast.LENGTH_SHORT).show();
        }
    
        @PermissionNo(REQUEST_CODE_PERMISSION_SD)
        private void getMultiNo(List<String> deniedPermissions) {
            Toast.makeText(this, R.string.message_post_failed, Toast.LENGTH_SHORT).show();
    
            // 用户否勾选了不再提示并且拒绝了权限,那么提示用户到设置中授权。
            if (AndPermission.hasAlwaysDeniedPermission(this, deniedPermissions)) {
                AndPermission.defaultSettingDialog(this, REQUEST_CODE_SETTING)
                        .setTitle(R.string.title_dialog)
                        .setMessage(R.string.message_permission_failed)
                        .setPositiveButton(R.string.btn_dialog_yes_permission)
                        .setNegativeButton(R.string.btn_dialog_no_permission, null)
                        .show();
    
                // 更多自定dialog,请看上面。
            }
        }
    
        //----------------------------------权限回调处理----------------------------------//
    
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[]
                grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            /**
             * 转给AndPermission分析结果。
             *
             * @param object     要接受结果的Activity、Fragment。
             * @param requestCode  请求码。
             * @param permissions  权限数组,一个或者多个。
             * @param grantResults 请求结果。
             */
            AndPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
        }
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            switch (requestCode) {
                case REQUEST_CODE_SETTING: {
                    Toast.makeText(this, R.string.message_setting_back, Toast.LENGTH_LONG).show();
                    //设置成功,再次请求更新
                    getVersion(Tools.getVersion(MainActivity.this));
                    break;
                }
            }
        }
    

    (3) 当apk文件下载完毕时,打开安装

       private void update() {
            //安装应用
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.fromFile(new File(Environment
                            .getExternalStorageDirectory(), DOWNLOAD_NAME)),
                    "application/vnd.android.package-archive");
            startActivity(intent);
        }
    

    Android 7.0 FileUriExposedException 的处理

    发现问题

    前几天把手机系统升级到基于 Android 7.0,后来在升级调试一个应用时抛出如下异常信息:

    android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.skyrin.bingo/cache/app/app.apk exposed beyond app through Intent.getData()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
    
    at com.skyrin.bingo.update.AppUpdate.installApk(AppUpdate.java:295)
    

    根据如上日志找到 AppUpdate 类下的 installApk 方法:

    /**
     * 安装apk
     */
    public static void installApk(Context context,String apkPath) {
        if (TextUtils.isEmpty(apkPath)){
            Toast.makeText(context,"更新失败!未找到安装包", Toast.LENGTH_SHORT).show();
            return;
        }
    
        File apkFile = new File(apkPath
                + apkCacheName);
    
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(
                Uri.fromFile(apkFile),
                "application/vnd.android.package-archive");
        context.startActivity(intent); 
    }
    

    问题出在启动安装程序阶段
    由于没升级 7.0 系统之前都没有问题,于是就在 Android 官网查看了一下 Android 7.0 新特性,终于发现其中 “在应用间共享文件” 一栏明确指出了这个问题

    解决问题

    官方给出的解决方式是通过 FileProvider 来为所共享的文件 Uri 添加临时权限,详细请看这里

    • 在 <application> 标签下添加 FileProvider 节点
    <application
       ...>
       ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.skyrin.bingo.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
       ...
    </application>
    
    

    android:authority 属性指定要用于 FileProvider 生成的 content URI 的 URI 权限,这里推荐使用 包名.fileprovider 以确保其唯一性。

    <provider><meta-data> 子元素指向一个 XML 文件,用于指定要共享的目录。

    • res/xml 目录下创建文件 file_paths.xml 内容如下:
    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-cache-path path="app/" name="apk"/>
    </paths>
    
    

    <external-cache-path> 表示应用程序内部存储目录下的 cache/ 目录,完整路径为 Android/data/com.xxx.xxx/cache/

    path 属性用于指定子目录。

    name 属性告诉 FileProvider 为 Android/data/com.xxx.xxx/cache/app/ 创建一个名为 apk 的路径字段。

    想要通过 FileProvider 为文件生成 content URI 只能在此处指定目录,以上示例就表示我将要共享 Android/data/com.xxx.xxx/cache/app/ 这个目录,除此之外还可以共享其它目录,对应的路径如下:

    标签 路径
    <files-path name="name" path="path" /> Context.getFilesDir()
    <cache-path name="name" path="path" /> getCacheDir()
    <external-path name="name" path="path" /> Environment.getExternalStorageDirectory()
    <external-files-path name="name" path="path" /> Context.getExternalFilesDir()
    <external-cache-path name="name" path="path" /> Context.getExternalCacheDir()
    • 完成以步骤后,我们修改出问题的代码如下:
    /**
     * 安装apk
     */
    public static void installApk(Context context,String apkPath) {
        if (TextUtils.isEmpty(apkPath)){
            Toast.makeText(context,"更新失败!未找到安装包", Toast.LENGTH_SHORT).show();
            return;
        }
    
        File apkFile = new File(apkPath
                + apkCacheName);
    
        Intent intent = new Intent(Intent.ACTION_VIEW);
        //Android 7.0 系统共享文件需要通过 FileProvider 添加临时权限,否则系统会抛出 FileUriExposedException .
        if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(context,"com.skyrin.bingo.fileprovider",apkFile);
            intent.setDataAndType(contentUri,"application/vnd.android.package-archive");
        }else {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(
                    Uri.fromFile(apkFile),
                    "application/vnd.android.package-archive");
        }
        context.startActivity(intent);
    }
    ...
    //调用,apkPath 入参就是 xml 中共享的路径
    String apkPath = context.getExternalCacheDir().getPath()+ File.separator+"app"+File.separator;
    AppUpdate.installApk(context,apkPath );
    
    

    结语

    除了上面这个问题,在 Android 7.0 之前开发的分享图文、浏览编辑本地图片、共享互传文件等功能如果没有使用 FileProvider 来生成 URI 的话,在 Android 7.0 上就必须做这种适配了,所以平时建议大家多关注 Android 新的 API ,尽早替换已被官方废弃的 API ,实际上 FileProvider 在 API Level 22 已经添加了。

    源码

    此demo已经上传到GitHub,如有需要自行下载
    GitHub:
    https://github.com/huangshuyuan/UpdateDemo/

    我的博客:
    http://blog.csdn.net/Imshuyuan/article/details/62886741

    我的简书:
    http://www.jianshu.com/p/2ab0459a9c3c

    相关文章

      网友评论

      本文标题:Android应用更新-自动检测版本及自动升级

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