Android6.0/7.0系统---使用Tomcat服务器进行版本检测更新

前言


自己写简书记录知识与看别人的简书、博客去学习有很大的区别,需要斟酌每一行代码的编写,认真的总结在编写代码过程中遇到的问题,避免以及提醒其他android开发者同样的问题。

其中的艰辛只有自己体会,但同时也受益匪浅。Android的开发学习就是在不断的总结、思考中,在不断的分享中才能得到进步。致那些无私奉献、不辞辛苦的开源开发者,博主,答主,简主。予人乐,其乐无穷。

代码无可期,梦想尤可违。但使志常存,亦复为君启。
PS:自己总结的一句话

本篇内容较多,涵盖的知识点较广,建议收藏学习。


Android6.0/7.0系统软件版本使用Tomcat服务器进行版本检测更新


版本的升级检测作为每个上线APP必备的功能,其重要性不言而喻。很多的开发者苦于不懂后端的开发,无法进行本地的版本测试、升级的操作。本篇将重点讲述在Android6.0/7.0及以上系统版本中,如何利用Tomcat服务器进行本地的软件版本的检测、更新。在上一篇中已经讲述如何利用Tomcat搭建本地服务器,还没有浏览这一篇的请自行前往: Android版本更新(一)---Tomcat服务器安装配置及问题解决。鉴于Android系统的开源性、资源的广泛性,以下内容中也会引用到其他开发者的相关博客、简书,如有侵权还请告知,并及时修改。
本章节将通过以下几个模块来介绍如何进行进行版本的检测、更新:

  • 服务器端的代码构建
  • 本地软件版本的获取
  • 对比服务器端版本弹出升级对话框
  • 下载并安装最新版本的软件

软件版本检测、更新的程序流程框图:

Paste_Image.png

最终的软件版本升级演示如下:

动态展示.gif

一、服务器端代码构建


服务器端的代码的构建比较简单,只需要创建一个json格式的文件(保存的字符标准选择为UTF-8,否则在后续的Gson解析的时候中文为乱码),如updateinfo.json,具体代码如下:

{
    "versionName":"2.0",
    "versionCode":2,
    "des":"这是升级后的版本",
    "apkUrl":"http://172.26.0.1:8181/app-release.apk"
}

其文件放置的位置为,tomcat根目录下,主要包含两个文件updateinfo.json和app-release.apk;
updateinfo.json---包含升级的相关信息
versionName:版本名,可用于在软件中显示的版本名称;
versionCode:版本号,用于本地versionCode与服务器端的对比;
des:版本升级的一些信息;
apkUrl:即升级的软件安装包app-release.apk所在的服务器端的位置连接;
app-release.apk---待升级的版本
以上两个文件可以仅作为测试使用,可以根据需求自行设置内容及文件名。

Paste_Image.png

打开Tomcat,并通过cmd的ipconfig获取本地的ip地址

Paste_Image.png

然后在浏览器输入: http://172.26.0.1:8181/updateinfo.json,即可显示我们的服务器的升级信息。
注意:这个是我本地的IP地址,请注意修改为你本地的IP地址,至于端口号为什么是8181,在上一篇中有说到,这个是由于本地的8080端口被占用,浏览器中的中文出现乱码,这个是浏览器的问题,不用理会。

Paste_Image.png

二、本地软件版本的获取


在项目project的AndroidManifest.xml中设置当前软件的版本信息versionCode和versionName

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test"
    android:versionName="1.0"
    android:versionCode="1">

同时需要module的defaultConfig配置versionCode和versionName

Paste_Image.png

构建工具类,来获取本地的versionCode和versionName

/**
* 获取本地的versionCode和versionName
*/
public class VersionGetUtil {
    private static final String TAG = "VersionGetUtil";
    /**
     * 获取版本名
     */
    public static String getVersionName(Context context){
        //PackageManager,可以获取清单中的所有信息
        PackageManager manager = context.getPackageManager();
        //getPackageName(),获取当前程序的包名
        try {
            //获取包中的信息
            PackageInfo info = manager.getPackageInfo(context.getPackageName(),0);
            String versionName = info.versionName;//版本名,是需要在APP中显示的
            Log.i(TAG, "getVersion: name" + versionName);
            return versionName;
        }catch (PackageManager.NameNotFoundException e){
            e.printStackTrace();
            Log.e("VersionGetUtil","can not get current Version Name");
        }
        //如果出现异常抛出null
        return null;
    }
    /**
     * 获取版本号
     */
    public static int getVersionCode(Context context){
        //PackageManager,可以获取清单中的所有信息
        PackageManager manager = context.getPackageManager();
        //getPackageName(),获取当前程序的包名
        try {
            //获取包中的信息
            PackageInfo info = manager.getPackageInfo(context.getPackageName(),0);
            int versionCode = info.versionCode;//版本号,用于判断是否为最新版本
            Log.i(TAG, "getVersion: code" + versionCode);
            return versionCode;
        }catch (PackageManager.NameNotFoundException e){
            e.printStackTrace();
            Log.e("VersionGetUtil","can not get current Version Code");
        }
        //如果出现异常抛出0
        return 0;
    }
}

三、对比服务器端版本弹出升级对话框等操作


首先根据服务器端的json格式的版本升级数据,封装成一个实体类VersionInfoEntity,便于后续使用OkHttp请求的时候解析,可以使用GsonFormat快速的实现实体类的生成,可以参考: 插件GsonFormat快速生成JSon实体类

public class VersionInfoEntity {

    /**
     * versionName : 2.0
     * versionCode : 2
     * des : 这是升级后的版本
     * apkUrl : http://172.26.0.1:8181/app-release.apk
     */

    private String versionName;
    private int versionCode;
    private String des;
    private String apkUrl;

    public String getVersionName() {
        return versionName;
    }

    public void setVersionName(String versionName) {
        this.versionName = versionName;
    }

    public int getVersionCode() {
        return versionCode;
    }

    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }

    public String getDes() {
        return des;
    }

    public void setDes(String des) {
        this.des = des;
    }

    public String getApkUrl() {
        return apkUrl;
    }

    public void setApkUrl(String apkUrl) {
        this.apkUrl = apkUrl;
    }
}

软件版本的升级,做法很简单:通过获取本地的版本号或者版本名与服务器端的版本号或者版本名进行对比,不一致则提示升级等操作,这里就只讲述版本号对比。考虑到其内部的流程比较多,且为了后续的维护,我的做法是封装成一个工具类,具体代码如下:
封装的工具类使用到了OkHttp,handler消息处理,对于各个函数的使用和关键代码都有注释,很容易理解,就不作一一说明,可以参考流程图对比代码,这样看起来就会一目了然。

public class VersionUpdateUtil extends Activity{
    private static final String TAG = VersionUpdateUtil.class.getSimpleName();
    /**
     * 服务器端保存版本更新信息的地址
     */
    private static final String server_url = "http://172.26.0.1:8181/updateinfo.json";

    /**
     * 用于构造函数
     * versionCode: 本地版本号
     */
    private Activity activity;
    private int currentVersionCode;

    /**
     * 版本更新实体类,包括版本号,版本名称,描述,下载地址
     */
    private VersionInfoEntity versionInfoEntity;
    /**
     * 服务器版本号,并初始化
     */
    private static int serverVersionCode = 1;

    /**
     * 声明okhttp客户端,并设置读/写/连接超时
     */
    private OkHttpClient client = new OkHttpClient.Builder()
            .readTimeout(10, TimeUnit.SECONDS) //设置读超时
            .writeTimeout(10, TimeUnit.SECONDS) //设置写超时
            .connectTimeout(10, TimeUnit.SECONDS) //设置连接超时范围
            .build();
    /**
     * 忽略版本,保存服务器端版本号到sp中
     */
    private SharedPreferences sharedPreferences = null;
    private SharedPreferences.Editor editor = null;
    private static int savedVersion = 1;

    /**
     * 进度更新dialog
     */
    private ProgressDialog progressDialog;

    /**
     * 请求码,用于动态权限设置的回调
     */
    private static final int REQUEST_CODE = 1;

    private File file = null;
    /**
     * handler消息的处理
     */
    private static final int UPDATE_YES = 1;
    private static final int UPDATE_NO = 2;
    private static final int IO_ERROR = 3;
    private static final int SHOW_DIALOG = 4;
    private static final int UPDATE_IGNORE = 5;
    private static final int UPDATE_PROGRESS = 6;
    private static final int UPDATE_INSTALL = 7;
    private static final int UPDATE_ADD_PERMISSION = 8;
    private static final int NEWEST_VERSION = 9;

    final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case UPDATE_YES:
                    Log.i(TAG, "handleMessage: 需要更新");
                    //从服务器端获取apk的下载网址
                    downLoadApk(versionInfoEntity.getApkUrl());
                    break;
                case UPDATE_NO:
                    Log.w(TAG, "handleMessage: 不需要更新");
                    break;
                case UPDATE_IGNORE:
                    Toast.makeText(activity,"用户忽略了该版本", Toast.LENGTH_SHORT).show();
                    saveNewestVersion(serverVersionCode);
                    break;
                case IO_ERROR:
                    Log.e(TAG, "handleMessage: IO异常");
                    break;
                case SHOW_DIALOG:
                    Log.i(TAG, "handleMessage: 弹出更新对话框");
                    showUpdateDialog();
                    break;
                case UPDATE_PROGRESS:
                    int progress = msg.arg1;
                    progressDialog.setProgress(progress);
                    break;
                case UPDATE_INSTALL:
                    Log.i(TAG, "handleMessage: file: " + file);
                    installApk(activity,file);
                    break;
                case UPDATE_ADD_PERMISSION:
                    Toast.makeText(activity,"需要添加权限,请点击允许", Toast.LENGTH_SHORT).show();
                    break;
                case NEWEST_VERSION:
                    Toast.makeText(activity,"已经是最新版本!", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };

    /**
     * 构造函数
     *
     * @param currentVersionCode 本地版本号
     * @param activity
     */
    public VersionUpdateUtil(int currentVersionCode, Activity activity) {
        this.activity = activity;
        this.currentVersionCode = currentVersionCode;
    }

    /**
     * 获取服务器版本号
     */
    public void getServerVersionCode() {
        //构造request,并设置request参数
        final Request request = new Request.Builder()
                .url(server_url)
                .build();

        //请求调度,异步get请求
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                handler.sendEmptyMessage(IO_ERROR);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

                String result = response.body().string();
                Log.i(TAG, "当前响应的结果:" + result);

                //利用gson解析服务器端的数据,并将数据保存到VersionInfoEntity实体类中
                Gson gson = new Gson();
                versionInfoEntity = gson.fromJson(result, VersionInfoEntity.class);

                //获取服务器端的版本号与本地的服务端版本号作对比
                serverVersionCode = versionInfoEntity.getVersionCode();

                Log.i(TAG, "服务器端版本: " + serverVersionCode);
                Log.i(TAG, "本地版本: " + currentVersionCode);
                Log.i(TAG, "sp保存的版本:" + savedVersion);
                /**
                 * 版本更新的判断与是否执行了忽略版本的操作
                 */
                if (serverVersionCode > currentVersionCode) {
                    if (serverVersionCode == savedVersion){
                        Log.i(TAG, "onResponse: 用户选择了忽略该版本");
                        handler.sendEmptyMessage(UPDATE_IGNORE);
                    }else {
                        handler.sendEmptyMessage(SHOW_DIALOG);
                    }

                } else {
                    System.out.println("无最新版本");
                    handler.sendEmptyMessage(NEWEST_VERSION);
                }
            }
        });
    }

    /**
     * 弹出对话框,让用户判断是否需要更新版本
     */
    private void showUpdateDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        builder.setTitle("监测到新版本");
        builder.setMessage(versionInfoEntity.getDes());
        builder.setPositiveButton("确定更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                handler.sendEmptyMessage(UPDATE_YES);
            }
        });
        builder.setNeutralButton("忽略版本", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                handler.sendEmptyMessage(UPDATE_IGNORE);
            }
        });
        builder.setNegativeButton("暂不更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int i) {
                handler.sendEmptyMessage(UPDATE_NO);
                dialog.dismiss();
            }
        });
        builder.create().show();
    }

    /**
     * 从服务器下载新版本的APK
     */
    private void downLoadApk(final String downLoadApkUrl) {
        //创建进度对话框
        createProgressDialog();

        //请求服务器端的apk
        final Request request = new Request.Builder()
                .url(downLoadApkUrl)
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                handler.sendEmptyMessage(IO_ERROR);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                /**
                 * android6.0系统后增加运行时权限,需要动态添加内存卡读取权限
                 */
                if (Build.VERSION.SDK_INT >= 23) {
                    int permission = ContextCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
                    if (permission != PackageManager.PERMISSION_GRANTED) {
                        progressDialog.dismiss();
                        ActivityCompat.requestPermissions(activity, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
                        Log.w(TAG, "checkWriteStoragePermission: 无此权限,需要添加");
                        handler.sendEmptyMessage(UPDATE_ADD_PERMISSION);
                        return;
                    } else {
                        downApkFlie(response);
                        if (progressDialog != null && progressDialog.isShowing()){
                            progressDialog.dismiss();
                        }
                        handler.sendEmptyMessage(UPDATE_INSTALL);

                    }
                } else {
                    downApkFlie(response);
                    if (progressDialog != null && progressDialog.isShowing()){
                        progressDialog.dismiss();
                    }
                    handler.sendEmptyMessage(UPDATE_INSTALL);
                }

            }
        });
    }


    /**
     * 忽略当前服务器端的版本
     * @param versionCode
     */
    private void saveNewestVersion(int versionCode) {
        sharedPreferences = activity.getSharedPreferences("ignore_ServerVersionCode", Activity.MODE_PRIVATE);
        editor = sharedPreferences.edit();

        editor.putInt("ignore_ServerVersionCode", versionCode);
        editor.commit();

        savedVersion = sharedPreferences.getInt("ignore_ServerVersionCode",versionCode);
    }

    private void createProgressDialog() {
        progressDialog = new ProgressDialog(activity);
        progressDialog.setMax(100);
        progressDialog.setCancelable(false);
        progressDialog.setMessage("正在下载");
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressDialog.show();
    }
    
    /**
     * OkHttp请求的结果
     *
     * @param response
     */
    private void downApkFlie(Response response) {
        InputStream is = null;
        FileOutputStream fos = null;
        byte[] buf = new byte[1024];//每次读取1K的数据

        int len = 0;
        long sum = 0;
        int progress = 0;

        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            file = new File(Environment.getExternalStorageDirectory(), "test.apk");
            try {
                if (file.exists()) {
                    file.delete();
                } else {
                    file.createNewFile();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long total = response.body().contentLength();
            Log.i(TAG, "downApkFlie: total---" + total);

            is = response.body().byteStream();

            //捕捉是否动态分配读写内存权限异常
            try {
                fos = new FileOutputStream(file);
                //捕捉输入流读取异常
                try {
                    /**
                     * read(),从输入流中读取数据的下一个字节,返回0~255范围内的字节值,如果已经到达
                     * 流末尾而没有可用的字节,则返回-1
                     */
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);//write(byte[]b, off, int len), 将指定的byte数组中从偏移量off开始的len个字节写入此输出流
                        sum += len;
                        progress = (int) (sum * 1.0f / total * 100);
                        Log.d("h_bl", "progress=" + progress);
                        //更新进度
                        Message msg = handler.obtainMessage();
                        msg.what = UPDATE_PROGRESS;
                        msg.arg1 = progress;
                        handler.sendMessage(msg);
                    }
                    fos.flush();//彻底完成输出并清空缓存区
                    Log.i(TAG, "downApkFlie: 下载完毕");
                } catch (IOException e) {
                    handler.sendEmptyMessage(IO_ERROR);
                }

            } catch (FileNotFoundException e) {
                e.printStackTrace();
                Log.e(TAG, "downApkFlie: 下载失败");
            } finally {
                //清空file输入输出流
                try {
                    if (is != null) {
                        is.close();//关闭输入流
                    }
                    if (fos != null) {
                        fos.close();//关闭输出流
                    }
                } catch (IOException e) {
                    handler.sendEmptyMessage(IO_ERROR);
                }
            }
        }
    }

    /**
     * 安装新版本APK
     */
    protected void installApk(Activity activity, File file) {
        if (activity == null || !file.exists()){
            return;
        }

        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于没有在Activity环境下启动Activity,设置下面的标签
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的权限问题
            //赋予临时权限
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //通过provider生成uri
            Uri contentUri = FileProvider.getUriForFile(activity, "com.example.test.fileprovider", file);//注意修改com.example.test为自己的包名
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
        }
        activity.startActivity(intent);
    }
}

在以上封装的工具类中,遇到了很多问题,重点讲述两个问题:

  1. 动态权限;
  2. Android7.0安装apk;

3.1 Android6.0动态权限

在上述工具类VersionInfoEntity.class中,在确定更新后,进入到downLoadApk(),从服务器端下载最新的apk。起初,我在Androidmanifest.xml中定义了以下权限

<!--相关权限-->
<!-- 权限设置 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 在SD卡中创建和删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS"/>
<!-- 向SD卡中写入东西权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>-->

<!-- 蓝牙 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />

<!-- 网络权限 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

这个习惯很不好,其实很多的权限,可能就用不到,但是经常怕遗忘,就全部拷贝过来了,那么问题就来了,在编译运行的时候,打印以下log:

Paste_Image.png

FileNotFoundException:/storage/emulated/0/test.apk,(Permission denied)

没有权限?可是我明明已经给与写内存卡的权限了啊,仔细地查询了一些资料,以及在android开发者官方: developer

Android6.0为了保护用户的隐私,将一些权限的申请放在了应用运行的时候去申请,如内存卡的读写权限。在以前的版本中,开发人员只需要在AndroidManifest.xml中设置即可,如

<!-- 向SD卡中写入东西权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

安装应用的时候可以在设置的应用信息中看到,如应用需要获得***权限,用户点击后就可以设置相应的权限,如允许、拒绝等。但是存在这一的一个问题,如一款应用APP只需要电话、短信的权限,但是在开发的过程中,开发者为了省事,请求了全部的权限,这就可能导致侵犯了用户隐私的权限请求,在用户安装了这款APP后,才发现拍照、读取内存、网络等权限被打开了,有可能导致一些隐私数据被剽窃。而Google官网为了避免用户的数据被剽窃,在Android6.0版本后加入了动态权限的的申请。 对于我这种喜欢全部权限都给予的人也是一种约束。

这些动态的权限在需要的时候才需要用户动态申请,比如在上面所说的APP中,如果需要用到拍照的功能,需要在使用的地方通过代码请求打开拍照权限的方式动态的去请求这个拍照权限。

以下是危险权限,有组的概念,如果一个权限组内的某个权限被获取了,那么这个组中剩余的权限也会被自动获取。而这些权限需要动态的去申请,可以理解为,动态的申请了

permission:android.permission.WRITE_EXTERNAL_STORAGE

group:android.permission-group.STORAGE,组中的其他权限也将自动获得。

以下是一些比较危险的权限,需要去动态的申请:

//联系人

group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS

//电话
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL

//日历
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR

//相机
group:android.permission-group.CAMERA
permission:android.permission.CAMERA

//传感器
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS

//定位
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION

//内存卡
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE

//耳机
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO

//SMS
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS

因此将从服务器端下载apk这一块需要用到动态权限的地方特殊的解释一下:代码如下:

/**
 * android6.0系统后增加运行时权限,需要动态添加内存卡读取权限
 */
if (Build.VERSION.SDK_INT >= 23) {
        int permission = ContextCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if (permission != PackageManager.PERMISSION_GRANTED) {
            progressDialog.dismiss();
            ActivityCompat.requestPermissions(activity, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
            Log.w(TAG, "checkWriteStoragePermission: 无此权限,需要添加");
            handler.sendEmptyMessage(UPDATE_ADD_PERMISSION);
            return;
        } else {
            downApkFlie(response);
            if (progressDialog != null && progressDialog.isShowing()){
                progressDialog.dismiss();
            }
            handler.sendEmptyMessage(UPDATE_INSTALL);

        }
    } else {
        downApkFlie(response);
        if (progressDialog != null && progressDialog.isShowing()){
            progressDialog.dismiss();
        }
        handler.sendEmptyMessage(UPDATE_INSTALL);
    }            
  • 检查权限

ContextCompat.checkSelfPermission(Context context, String permission);

  • 有权限: PackageManager.PERMISSION_GRANTED
  • 无权限: PackageManager.PERMISSION_DENIED

当应用需要用到危险权限时,在执行权限相关代码前,使用该方法判断是否拥有指定的权限。有权限,则继续执行设计需要权限的代码;无权限,则向用户请求授予权限。

  • 解释权限

ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission)

判断是否有必要向用户解释为什么要这项权限。如果应用第一次请求过此权限,但是被用户拒绝了,则之后调用该方法将返回 true,此时就有必要向用户详细说明需要此权限的原因
备注:如果应用第一次请求此权限时被用户拒绝,第二次再请求此权限时,用户勾选了权限请求对话框的“不再询问”,则此方法返回 false。如果设备规范禁止应用拥有该权限,此方法也返回 false。

  • 请求权限

ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode)

当检测到应用没有指定的权限时,调用此方法向用户请求权限。调用此方法将弹出权限请求对话框询问用户 “允许” 或 “拒绝” 指定的权限。

  1. 权限参数传入的是数组,可以调用该方法一次请求多个权限;传入的权限数组参数以单个具体权限为单位,但弹框询问用户授权时,属于同一权限组的权限将自动合并询问授权一次;
  2. 请求的权限必须事先在 AndroidManifest.xml 中有声明,否则调用此方法请求时,将不弹框,而是直接返回“拒绝”的结果;
  3. 第一次请求权限时,用户点击了“拒绝”,第二次再请求该权限时,对话框将出现“不再询问”复选框,如果用户勾选了“不再询问”并点击了“拒绝”,则之后再请求此权限组时将不弹框,而是直接返回“拒绝”的结果。
  • 处理结果
    请求权限的结果返回和接收一个Activity的返回类似,重写 FragmentActivity 或 (v4) Fragment 中的 onRequestPermissionsResult(...) 方法。
    /**
     * 处理权限请求结果
     *
     * @param requestCode
     *          请求权限时传入的请求码,用于区别是哪一次请求的
     *
     * @param permissions
     *          所请求的所有权限的数组,
     *          例如String permissions = new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
     *                           android.Manifest.permission.CALL_PHONE};
     *
     * @param grantResults
     *          权限授予结果,和 permissions 数组参数中的权限一一对应,
     *          例,WRITE_EXTERNAL_STORAGE和CALL_PHONE两个元素值为两种情况,如下:
     *          授予: PackageManager.PERMISSION_GRANTED
     *          拒绝: PackageManager.PERMISSION_DENIED
     *          可能的结果有几种,{true,true},{true,false},{false,true},{false,false}
     *          如果针对某一个直接采用数组下标来判断,例如WRITE_EXTERNAL_STORAGE,则为grantResults[0],代表其权限值
     *
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            Log.i(TAG, "onRequestPermissionsResult: ++++");
        }
    }

3.2 android7.0安装APK

在apk下载后,进入安装阶段出现以下error

FATAL EXCEPTION: main
Process: com.example.test.release, PID: 19544
android.os.FileUriExposedException: file:///storage/emulated/0/test.apk exposed beyond app through Intent.getData()

搜寻了一些资料:Android7.0行为变更,这是由于Android7.0执行了“StrictMode API 政策禁”以及“私有目录被限制访问”的原因,随着Android版本越来越高,Google对于用户隐私的保护力度也越来越大。可以用FileProvider来解决这一问题。

在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。

” StrictMode API 政策” 是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。

现在我们就来一步一步的解决这个问题。

3.2.1 AndroidManifest.xml清单文件中注册provider

provider也是Android四大组件之一,可以简单把它理解为向外提供数据的组件,参考: FileProvider API:在项目的AndroidManifest.xml中注册。具体配置代码如下:

<application>
...
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.test.fileprovider"
    android:grantUriPermissions="true"
    android:exported="false"
    >
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
...
</application>

其中

android:authorities:组件标识,这个属性的com.example.test为你本项目的包名,可以在mainfest中找到,用于避免和其它应用发生冲突

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test"
    android:versionName="1.0"
    android:versionCode="1">

android:resource,指的是当前组件引用 res/xml/file_paths.xml 这个文件,通过<meta-data>标签将上面的filepath添加到provider当中

通过阅读API,放置<paths>元素和子元素到项目中的xml文件,需自行创建

Paste_Image.png

新建的file_paths.xml的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="test" path="."/>
</paths>

通过阅读API:

<files-path/>代表的根目录:Context.getFilesDir()

<external-path/>代表的根目录: Environment.getExternalStorageDirectory()

<cache-path/>代表的根目录: getCacheDir()

在上述代码中:

path="."代表的是根目录,即你可以向其它的应用共享根目录及其子目录下任何一个文件了。如果使用path="download",那么得到的目录为“/storage/emulated/0/download”,只允许向其他应用共享download目录及其子目录内的文件。

在完成以上配置后,使用到的完整的安装apk的函数如下:

/**
     * 安装新版本APK
     */
    protected void installApk(Activity activity, File file) {
        if (activity == null || !file.exists()){
            return;
        }

        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于没有在Activity环境下启动Activity,设置下面的标签
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的权限问题
            //赋予临时权限
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //通过provider生成uri
            Uri contentUri = FileProvider.getUriForFile(activity, "com.example.test.fileprovider", file);//注意修改com.example.test为自己的包名
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
        }
        activity.startActivity(intent);
    }

特别需要注意的是:需要注意的是:修改com.example.test为自己的包名。


四、测试

  • 测试手机:三星S7 edge
  • 服务器:Tomcat:http://172.26.0.1:8181/updateinfo.json
  • 软件版本:1.0
  • 服务端软件版本:2.0
  • 更改内容:主界面新增加一个textView“这是升级后的版本”。

4.1 修改AndroidManifest.xml及build.gradle配置文件

AndroidManifest.xml修改为:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test"
    android:versionName="2.0"
    android:versionCode="2">

build.gradle文件修改为:

defaultConfig {
    applicationId "com.example.test"
    minSdkVersion 19
    targetSdkVersion 25
    versionCode 2
    versionName "2.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

4.2 主界面activity_main.xml

新增textView2:“这是升级后的版本”

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.test.MainActivity">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="检查更新" />
    <TextView
        android:id="@+id/test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@color/colorAccent"
        android:layout_centerInParent="true" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/test"
        android:textSize="30sp"
        android:textStyle="bold"
        android:textColor="@color/colorAccent"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="49dp"
        android:text="这是升级后的版本" />

</RelativeLayout>

4.3 MainActivity.java

主界面就一个button用于执行版本检测

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private int currentVersionCode;
    private String currentVersionName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //获取本地版本号和版本名
        currentVersionCode = VersionGetUtil.getVersionCode(MainActivity.this);
        currentVersionName = VersionGetUtil.getVersionName(MainActivity.this);
        Log.i(TAG, "onCreate: 版本号:" + currentVersionCode + ",版本名:" + currentVersionName);

        final VersionUpdateUtil updateUtil = new VersionUpdateUtil(currentVersionCode,MainActivity.this);
              Button button = (Button)findViewById(R.id.button);
                button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.i(TAG,"开始执行版本判断");
                updateUtil.getServerVersionCode();
            }
        });
        TextView textView = (TextView)findViewById(R.id.test);
        textView.setText("当前版本: " + currentVersionName);
    }
}

4.4 测试

这个是重中之重,请务必保持手机与Tomcat处于同一局域网下,可以采用360随身wifi,否则会出现无法请求的错误,直接提示IO异常。原因可以参考:Android真机连接本地部署的Tomcat问题,将最新版本的APK放到Tomcat中,开始手机端的测试工作。具体的测试请参考视频。


五、总结

经过连续几天的摸索与整理,总算把版本更新的代码、文档整理完毕,受益匪浅。当然还存在着很多的不足:

  • 没有提示移动网络环境及wifi环境的下载提示;
  • 没有对动态申请权限做进一步的处理,如点击“拒绝”,“不在询问”,的处理;
  • 没有进行自动检测版本的设置;
  • 类的封装,代码不够精简;
  • SD卡外挂判断还需要进一步的整合;

知识的梳理,语言的整理,流程化的处理,还需要进一步的完善,每天进步一点点。也希望各位developer共同探讨,对于以上出现的各个问题,希望不吝赐教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 因不胜抑郁症折磨,而自行结束生命的男明星乔任梁,近日他生前最好的朋友陈乔恩,在乔任梁追悼会举行当日,发表了一篇长微...
    破布鞋开口笑阅读 322评论 0 0
  • 2017年3月14日 我曾经一万次在脑海中幻想过我成为记者的模样,胸前挂着让我从踏入大学校园就向往的记者证,背着象...
    果子__你好阅读 2,185评论 0 1
  • 嗨,我是文案妞,你是哪一位呢?今天过的好吗? 每晚22:00,想说情话给你听,让我们一起卸下负累,休憩心灵…… 今...
    文案妞阅读 411评论 0 1