天天看点

Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 & 更新 & 打包

文章目录

  • ​​1.手机防盗需求分析​​
  • ​​2.手机卫士其余模块需求分析​​
  • ​​3.手机卫士的包名划分​​
  • ​​4.SVN提交代码 & 代码的下载​​
  • ​​5.SplashActivity布局分析​​
  • ​​6.文本框阴影效果​​
  • ​​7.Activity去头操作 & 保留高版本主题​​
  • ​​8.获取版本名称并且展示​​
  • ​​9.构建服务器端json & 无BOM编码​​
  • ​​10.请求网络数据 & 测试​​
  • ​​11.json解析过程​​
  • ​​12.断点调试json解析错误过程​​
  • ​​13.消息机制 & 发送不同类型的消息​​
  • ​​14.弹出对话框​​
  • ​​15.xUtils说明 & 下载方法使用​​
  • ​​16.打包生成apk & 维护到服务器​​
  • ​​17.签名文件说明 & 包名说明​​
  • ​​18.安装过程中点击取消​​

1.手机防盗需求分析

首先应用要有一个闪屏页面,闪屏页面(SplashActivity)会有以下的功能:

  • 版本名称的展示
  • 服务端新版本的检测
  • 展示logo

而手机防盗功能,需要有以下的功能:

  • sim卡绑定:每一款手机都会有相应的卡的序列号(唯一性),一旦替换掉原有电话卡,序列号会发生改变
  • GPS追踪
  • 远程锁屏
  • 数据销毁

防盗模块大体功能图如下:

Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 & 更新 & 打包

2.手机卫士其余模块需求分析

其他模块包括黑名单管理,软件管理、进程管理、流量统计、手机杀毒、缓存清理、高级工具、设置中心等模块,模块图大概如下所示:

Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 & 更新 & 打包

高级工具的功能包括:

  • 归属地查询
  • 短信备份
  • 常用号码查询
  • 程序锁

3.手机卫士的包名划分

每一个类的包名,都应该按照相应的需求进行命名,一些常见的划分类型如下所示:

  • 按照组成方式:命名方式为​

    ​com.itheima.db​

  • 按照业务逻辑方式:命名方式为​

    ​com.icbc.money​

    ​​、​

    ​com.icbc.meeting​

  • 按照组件划分:主要以四大组件为主,包括Activity、Service、ContentProvider、BroadCastReceiver,命名方式为​

    ​com.itheima.activity​

    ​​、​

    ​com.itheima.service​

该工程我们将按组件划分的方式进行项目的实现

4.SVN提交代码 & 代码的下载

虽然目前主流的版本管理工具都使用Git,但这里我们尝试使用SVN,来实现代码仓库的托管以及相应的管理

具体的操作流程可以参考网上,这里不再赘述

5.SplashActivity布局分析

  1. 为了在后期维护中加上版本号等相关属性名,我们需要在manifest.xml中进行相应的配置,修改androidmanifest.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mobilesafe"
    android:versionCode="1"
    android:versionName="1.0.0">
    <!-- android:versionCode 本地应用版本号是1,版本号是2,有必要提示用户更新 -->
    <!-- android:versionName 本地应用版本名,假设版本名为1.0.0,它们所代表的意义如下:
                第一位:项目有重大更新(代码重构,大部分功能添加,界面整体添加)
                第二位:更新部分功能
                第三位:一般代表修复原有版本的bug -->

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".activity.SplashActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>      
  1. 在项目下新建activity包,存放activity,然后将MainActivity改名为SplashActivity,同时将布局文件修改为activity_splash.xml,作为闪屏页面
  2. 修改activity_splash.xml,使用相对布局,并使用相应的背景图片,代码如下:
<?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"
    android:background="@drawable/launcher_bg"
    tools:context=".activity.SplashActivity">

    <TextView
        android:id="@+id/tv_version_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="版本名称"/>

</RelativeLayout>      

6.文本框阴影效果

为了美化闪屏页面的显示效果,这里我们进行一些文本框阴影效果的处理,预览图如下所示:

Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 &amp; 更新 &amp; 打包

修改activity_splash.xml,给TextView控件添加实现阴影效果的标签(android:shadow),并且添加一个进度条组件,注意调整位置,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/launcher_bg"
    tools:context=".activity.SplashActivity">

    <!-- android:shadowRadius="5" 表示阴影所在范围   -->
    <TextView
        android:id="@+id/tv_version_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:shadowDx="1"
        android:shadowDy="1"
        android:shadowColor="#f00"
        android:shadowRadius="5"
        android:text="版本名称"/>

    <ProgressBar
        android:layout_below="@id/tv_version_name"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>      

7.Activity去头操作 & 保留高版本主题

因为SplashActivity是没有标题栏的,所以需要进行相应的设置,实现的方式有很多:

  • 代码中修改
  • manifest中修改
  • style中修改

由于前两种方式都不灵活,这里采用第三种方式进行修改

进入​

​@style/Theme.AppCompat.Light.NoActionBar​

​查看源码,我们可以发现该样式中控制标题栏是否显示的语句为:

<item name="windowNoTitle">true</item>      

因为当前Android应用默认使用​

​@style/AppTheme​

​样式,所以只需要进入该样式并加入上面的语句,即可显示相同的效果,代码如下:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">"colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="windowNoTitle">true</item>
    </style>

</resources>      

这样,就可以实现在AppTheme样式中没有标题栏的效果了。与此同时,也可以保留高版本的样式主题,一举两得

8.获取版本名称并且展示

由于版本号是非固定的,而是随着后期开发逐渐变化的,所以想要显示版本号就需要编写相应的逻辑

  1. 修改SplashActivity,新增initUI()方法,作为UI初始化的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SplashActivity extends AppCompatActivity {

    private TextView tv_version_name;

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

        // 初始化UI
        initUI();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }
}      
  1. 修改SplashActivity,新增initData()方法,作为数据初始化的方法,并且再新增getVersionName()方法,作为获取版本号名称的逻辑封装,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SplashActivity extends AppCompatActivity {

    private TextView tv_version_name;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }
}      

注意:如果使用的是Android Studio作为IDE,这里要修改versionName时光修改manifest.xml时不生效,要同步修改build.gradle中的versionName才生效

9.构建服务器端json & 无BOM编码

这一步我们需要搭建服务器,以向服务器获取相应数据并进行更新,更新界面类似如下:

Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 &amp; 更新 &amp; 打包
  1. 修改SplashActivity,添加getVersionCode(),作为获取本地版本号的方法,放到initData()方法中,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地版本号
        mLocalVersionCode = getVersionCode();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }
}      
  1. 编写JSON文件,作为提醒客户端进行更新的相应文件,这里使用HiJson来进行相应的编写,注意最后要保存为json格式的不带BOM编码的UTF-8编码格式,因为该格式在Android2.2版本以后会出现解析上的问题,JSON代码如下:
{
    "versionCode": "2",
    "versionDes": "2.0版本发布了!",
    "versionName": "2.0",
    "downloadUrl": "www.abc.com"
}      
  1. 将刚刚编写好的JSON文件放到Tomcat服务器中,这里为了方便测试,直接将该文件(作者这里将该文件起名为update74.json)放到webapps/ROOT目录下即可
  2. 启动Tomcat服务器,直接到bin目录下双击startup.bat,出现以下界面即可说明服务器启动成功,如图所示:
Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 &amp; 更新 &amp; 打包

10.请求网络数据 & 测试

上一节中我们编写了JSON文件,这里需要对该文件进行一个简单的测试,看是否能够调用

  1. 修改SplashActivity,添加checkVersion(),作为获取服务端版本号的方法,放到initData()方法中,注意这里的请求地址要改成你的电脑的IP地址或者Google推荐的10.0.2.2(仅限模拟器访问电脑上的TomCat),而非localhost,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
        1.更新版本的名称 versionName
        2.新版本的描述信息 versionDes
        3.服务器的版本号 versionCode
        4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        new Thread(){
            @Override
            public void run() {
                // 发送请求,获取数据,参数则为请求json的链接地址
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}      
  1. 在包下新建utils包,并在该包下新建StreamUtil,作为将流转换为字符串的工具类,代码如下:
package com.example.mobilesafe.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class StreamUtil {

    /**
     * 将流转换成字符串
     * @param is 流对象
     * @return 流转换成的字符串,返回null代表异常
     */
    public static String streamToString(InputStream is) {
        // 1.在读取的过程中,将读取的内容存储至缓存中,然后一次性的转换成字符串返回
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        // 2.读取流,读到没有为止(循环)
        byte[] buffer = new byte[1024];
        // 3.记录读取内容的临时变量
        int temp = -1;
        try {
            while ((temp = is.read(buffer)) != -1){
                bos.write(buffer,0,temp);
            }
            // 4.返回读取的数据
            return bos.toString();
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                is.close();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}      
  1. 由于涉及到网络操作,需要在manifest.xml中声明权限,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mobilesafe"
    android:versionCode="1"
    android:versionName="1.0.0">
    <!-- android:versionCode 本地应用版本号是1,版本号是2,有必要提示用户更新 -->
    <!-- android:versionName 本地应用版本名,假设版本名为1.0.0,它们所代表的意义如下:
                第一位:项目有重大更新(代码重构,大部分功能添加,界面整体添加)
                第二位:更新部分功能
                第三位:一般代表修复原有版本的bug -->
    
    <!-- 添加网络权限 -->
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".activity.SplashActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>      
  1. 运行应用,进行相应测试(可以使用Log类),输出结果如图所示:
Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 &amp; 更新 &amp; 打包

11.json解析过程

之前我们已经拿到了从流转化为字符串的文本,现在需要将这段文本转换为json格式

修改SplashActivity,添加json解析的相应逻辑,这里使用JSONObject来解析,建议使用Log类打印日志,观察是否能够解析成功,代码如下:

package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
        1.更新版本的名称 versionName
        2.新版本的描述信息 versionDes
        3.服务器的版本号 versionCode
        4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        new Thread(){
            @Override
            public void run() {
                // 发送请求,获取数据,参数则为请求json的链接地址
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        String versionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        String downloadUrl = jsonObject.getString("downloadUrl");
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                }catch (IOException e) {
                    e.printStackTrace();
                }
                catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}      

12.断点调试json解析错误过程

如果在解析json数据中,存在一些问题,那么可能会导致json的解析出错,这时候我们就需要通过断点调试的方式来进行排错

具体排错过程可参考百度,这里不再详述

13.消息机制 & 发送不同类型的消息

因为在比对版本号之后需要弹出“更新”的对话框,由于是UI操作,而Android规定不能在子线程更新UI,所以这里我们需要使用消息机制来发送消息,通知主线程进行UI操作

  1. 修改SplashActivity,添加消息机制,并在checkVersion()方法中处理相应逻辑,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
        1.更新版本的名称 versionName
        2.新版本的描述信息 versionDes
        3.服务器的版本号 versionCode
        4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        String versionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        String downloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }
}      
  1. 修改SplashActivity,添加enterHome(),作为进入应用程序主界面的方法,同时在activity包下创建HomeActivity,作为主界面,暂时套用EmptyAcivity模板即可,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
        1.更新版本的名称 versionName
        2.新版本的描述信息 versionDes
        3.服务器的版本号 versionCode
        4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        String versionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        String downloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }
}      
  1. 修改SplashActivity,修改checkVersion()方法,为了优化视觉效果,将引导页面跳转到主页面的时间设置为4秒,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
        1.更新版本的名称 versionName
        2.新版本的描述信息 versionDes
        3.服务器的版本号 versionCode
        4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        String versionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        String downloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }
}      

14.弹出对话框

上面我们增加了相应的消息机制,这一节来继续完善机制,并添加相应的UI操作

  1. 在utils包下新建ToastUtil,作为弹出对话框的工具类,方便之后调用,代码如下:
package com.example.mobilesafe.utils;

import android.content.Context;
import android.widget.Toast;

public class ToastUtil {

    /**
     * Toast打印
     * @param ctx 上下文
     * @param msg 打印文本内容
     */
    public static void show(Context ctx,String msg){
        Toast.makeText(ctx, msg,Toast.LENGTH_SHORT).show();
    }
}      
  1. 修改SplashActivity,在Handler中添加每个分支的处理逻辑,使用刚刚编写好的工具类,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    ToastUtil.show(SplashActivity.this,"url异常");
                    enterHome();
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    ToastUtil.show(SplashActivity.this,"IO异常");
                    enterHome();
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    ToastUtil.show(SplashActivity.this,"json异常");
                    enterHome();
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
            1.更新版本的名称 versionName
            2.新版本的描述信息 versionDes
            3.服务器的版本号 versionCode
            4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        String versionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        String downloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }
}      
  1. 修改SplashActivity,添加showUpdateDialog(),作为显示更新对话框的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * 更新时描述信息
     */
    private String mVersionDes;

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    showUpdateDialog();
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    ToastUtil.show(SplashActivity.this,"url异常");
                    enterHome();
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    ToastUtil.show(SplashActivity.this,"IO异常");
                    enterHome();
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    ToastUtil.show(SplashActivity.this,"json异常");
                    enterHome();
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
            1.更新版本的名称 versionName
            2.新版本的描述信息 versionDes
            3.服务器的版本号 versionCode
            4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        mVersionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        String downloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }

    /**
     * 7.弹出更新对话框
     */
    private void showUpdateDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
        builder.setTitle("版本更新"); // 设置标题
        builder.setMessage(mVersionDes); // 设置描述内容
        builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 下载apk,需要apk的链接地址,即downloadUrl
            }
        });// 积极按钮,“是”
        builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 取消对话框,进入主界面
                enterHome();
            }
        });// 消极按钮,“否”
        builder.show();
    }
}      

15.xUtils说明 & 下载方法使用

完善了消息机制后,接下来我们就需要处理“下载”逻辑的相关代码,这里的下载逻辑使用“多线程下载”,即将某个文件划分成3份,每一份都有相应的线程去做下载,记录当前的下载位置,下一次就在当前记录的下载位置继续下载

为了便于下载逻辑的实现,这里还使用到一个框架:xUtils,这是aFinal框架的前身,感兴趣的读者可以去百度查阅

xUtils的集成只需要直接导入jar包,并且添加相应权限即可,这里不再详述

修改SplashActivity,修改showUpdateDialog()方法,新建downloadApk()方法来完善下载逻辑,代码如下:

package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * 更新时描述信息
     */
    private String mVersionDes;

    /**
     * 更新时的URL
     */
    private String mDownloadUrl;

    private static final String tag = "SplashActivity";

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    showUpdateDialog();
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    ToastUtil.show(SplashActivity.this,"url异常");
                    enterHome();
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    ToastUtil.show(SplashActivity.this,"IO异常");
                    enterHome();
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    ToastUtil.show(SplashActivity.this,"json异常");
                    enterHome();
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
            1.更新版本的名称 versionName
            2.新版本的描述信息 versionDes
            3.服务器的版本号 versionCode
            4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        mVersionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        mDownloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }

    /**
     * 7.弹出更新对话框
     */
    private void showUpdateDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
        builder.setTitle("版本更新"); // 设置标题
        builder.setMessage(mVersionDes); // 设置描述内容
        builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 下载apk,需要apk的链接地址,即downloadUrl
                downloadApk();
            }
        });// 积极按钮,“是”
        builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 取消对话框,进入主界面
                enterHome();
            }
        });// 消极按钮,“否”
        builder.show();
    }

    /**
     * 8.APK下载
     */
    private void downloadApk() {
        // 需要apk下载链接地址,放置apk的所在路径
        // 1.判断sd卡是否可用,是否挂载
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            // 2.获取sd卡路径
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "mobilesafe74.apk";
            // 3.发送请求,获取Apk,并且放置到指定路径(下载地址,下载应用的放置位置,)
            HttpUtils httpUtils = new HttpUtils();
            httpUtils.download(mDownloadUrl, path, new RequestCallBack<File>() {
                @Override
                public void onSuccess(ResponseInfo<File> responseInfo) {
                    // 下载成功(下载过后的放置在sd卡中apk)
                    Log.i(tag,"下载成功!");
                    File file = responseInfo.result;
                }

                @Override
                public void onFailure(HttpException e, String s) {
                    // 下载失败
                    Log.i(tag,"下载失败!");
                }

                @Override
                public void onStart() {
                    // 刚刚开始下载
                    Log.i(tag,"刚刚开始下载!");
                    super.onStart();
                }

                @Override
                public void onLoading(long total, long current, boolean isUploading) {
                    // 下载过程(下载文件大小,当前的下载位置,是否正在下载)
                    Log.i(tag,"下载中......文件大小为" + total + "当前的下载位置为" + current);
                    super.onLoading(total, current, isUploading);
                }
            });
        }
    }
}      

16.打包生成apk & 维护到服务器

下载逻辑完善的同时,我们需要将该(高版本)的应用打包成apk,并且维护到服务器上

打包的过程可以参考百度,这里仅作简单陈述:

  1. 生成签名文件,并且指定所在位置
  2. 使用生成的签名文件,给工程打包生成一个apk

打包成功后,只需要放置到跟之前的json文件同一个tomcat中的目录,即可将apk文件维护到服务器上

随后,修改json文件,将路径改成如图所示(apk文件因人而异,最好配置完成后可以去网页上测试一下,看能否访问到这个文件),运行文件,进行相应测试:

Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 &amp; 更新 &amp; 打包

17.签名文件说明 & 包名说明

下载完成后,就应该提示用户安装这个apk,但是在安装后会提示“已安装了存在签名冲突的同名数据包”,所以我们需要分析签名文件和包名

  1. 修改SplashActivity,修改downloadApk()方法,新建installApk()方法来完善安装apk逻辑(使用隐式意图来启动系统的apk安装界面),代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * 更新时描述信息
     */
    private String mVersionDes;

    /**
     * 更新时的URL
     */
    private String mDownloadUrl;

    private static final String tag = "SplashActivity";

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    showUpdateDialog();
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    ToastUtil.show(SplashActivity.this,"url异常");
                    enterHome();
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    ToastUtil.show(SplashActivity.this,"IO异常");
                    enterHome();
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    ToastUtil.show(SplashActivity.this,"json异常");
                    enterHome();
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
            1.更新版本的名称 versionName
            2.新版本的描述信息 versionDes
            3.服务器的版本号 versionCode
            4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        mVersionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        mDownloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }

    /**
     * 7.弹出更新对话框
     */
    private void showUpdateDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
        builder.setTitle("版本更新"); // 设置标题
        builder.setMessage(mVersionDes); // 设置描述内容
        builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 下载apk,需要apk的链接地址,即downloadUrl
                downloadApk();
            }
        });// 积极按钮,“是”
        builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 取消对话框,进入主界面
                enterHome();
            }
        });// 消极按钮,“否”
        builder.show();
    }

    /**
     * 8.APK下载
     */
    private void downloadApk() {
        // 需要apk下载链接地址,放置apk的所在路径
        // 1.判断sd卡是否可用,是否挂载
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            // 2.获取sd卡路径
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "app-release.apk";
            Log.i(tag,"路径为:" + path);
            // 3.发送请求,获取Apk,并且放置到指定路径(下载地址,下载应用的放置位置,回调方法)
            HttpUtils httpUtils = new HttpUtils();
            httpUtils.download(mDownloadUrl, path, new RequestCallBack<File>() {
                @Override
                public void onSuccess(ResponseInfo<File> responseInfo) {
                    // 下载成功(下载过后的放置在sd卡中apk)
                    Log.i(tag,"下载成功!");
                    File file = responseInfo.result;
                    installApk(file);
                }

                @Override
                public void onFailure(HttpException e, String s) {
                    // 下载失败
                    Log.i(tag,"下载失败!");
                    e.printStackTrace();
                }

                @Override
                public void onStart() {
                    // 刚刚开始下载
                    Log.i(tag,"刚刚开始下载!");
                    super.onStart();
                }

                @Override
                public void onLoading(long total, long current, boolean isUploading) {
                    // 下载过程(下载文件大小,当前的下载位置,是否正在下载)
                    Log.i(tag,"下载中......文件大小为" + total + "当前的下载位置为" + current);
                    super.onLoading(total, current, isUploading);
                }
            });
        }
    }

    /**
     * 9.APK安装
     * @param file 安装文件
     */
    private void installApk(File file) {
        // 系统应用界面,源码,安装apk入口
        Intent intent = new Intent("android.intent.action.VIEW");
        intent.addCategory("android.intent.category.DEFAULT");
        // 文件作为数据源
        // intent.setData(Uri.fromFile(file));
        // 设置安装的类型
        // intent.setType("application/vnd.android.package-archive");
        intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
        startActivity(intent);
    }
}      
  1. 运行后,安装时会报错,原因如下:
  1. 包名一致(√):由于该下载会下载一个同签名,同包名的APK文件,就会导致之前提示的错误,这里需要的更新操作要覆盖掉旧的apk,所以需要保持包名一致
  2. 签名一致(×):直接在模拟器上运行的应用(旧版本),签名文件为debug.keystore,而新版本的签名文件在打包的过程中使用的是新的,所以实质上是不一致的
  1. 所以,为了保持签名一致,我们需要让旧版本的签名文件也跟新版本的签名文件一致,所以对旧版本专门进行一次打包,然后再安装到模拟器上,即可完成正常的安装

18.安装过程中点击取消

为了防止用户点击回退后出现bug,所以需要进一步完善相关逻辑

  1. 修改SplashActivity,修改showUpdateDialog()方法,添加按下回退按钮的事件监听,代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * 更新时描述信息
     */
    private String mVersionDes;

    /**
     * 更新时的URL
     */
    private String mDownloadUrl;

    private static final String tag = "SplashActivity";

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    showUpdateDialog();
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    ToastUtil.show(SplashActivity.this,"url异常");
                    enterHome();
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    ToastUtil.show(SplashActivity.this,"IO异常");
                    enterHome();
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    ToastUtil.show(SplashActivity.this,"json异常");
                    enterHome();
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
            1.更新版本的名称 versionName
            2.新版本的描述信息 versionDes
            3.服务器的版本号 versionCode
            4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        mVersionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        mDownloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }

    /**
     * 7.弹出更新对话框
     */
    private void showUpdateDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
        builder.setTitle("版本更新"); // 设置标题
        builder.setMessage(mVersionDes); // 设置描述内容
        builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 下载apk,需要apk的链接地址,即downloadUrl
                downloadApk();
            }
        });// 积极按钮,“是”
        builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 取消对话框,进入主界面
                enterHome();
            }
        });// 消极按钮,“否”
        builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                // 按下回退后,进入主界面,然后隐藏对话框
                enterHome();
                dialog.dismiss();
            }
        });// 回退按钮
        builder.show();
    }

    /**
     * 8.APK下载
     */
    private void downloadApk() {
        // 需要apk下载链接地址,放置apk的所在路径
        // 1.判断sd卡是否可用,是否挂载
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            // 2.获取sd卡路径
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "app-release.apk";
            Log.i(tag,"路径为:" + path);
            // 3.发送请求,获取Apk,并且放置到指定路径(下载地址,下载应用的放置位置,回调方法)
            HttpUtils httpUtils = new HttpUtils();
            httpUtils.download(mDownloadUrl, path, new RequestCallBack<File>() {
                @Override
                public void onSuccess(ResponseInfo<File> responseInfo) {
                    // 下载成功(下载过后的放置在sd卡中apk)
                    Log.i(tag,"下载成功!");
                    File file = responseInfo.result;
                    installApk(file);
                }

                @Override
                public void onFailure(HttpException e, String s) {
                    // 下载失败
                    Log.i(tag,"下载失败!");
                    e.printStackTrace();
                }

                @Override
                public void onStart() {
                    // 刚刚开始下载
                    Log.i(tag,"刚刚开始下载!");
                    super.onStart();
                }

                @Override
                public void onLoading(long total, long current, boolean isUploading) {
                    // 下载过程(下载文件大小,当前的下载位置,是否正在下载)
                    Log.i(tag,"下载中......文件大小为" + total + "当前的下载位置为" + current);
                    super.onLoading(total, current, isUploading);
                }
            });
        }
    }

    /**
     * 9.APK安装
     * @param file 安装文件
     */
    private void installApk(File file) {
        // 系统应用界面,源码,安装apk入口
        Intent intent = new Intent("android.intent.action.VIEW");
        intent.addCategory("android.intent.category.DEFAULT");
        // 文件作为数据源
        // intent.setData(Uri.fromFile(file));
        // 设置安装的类型
        // intent.setType("application/vnd.android.package-archive");
        intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
        startActivity(intent);
    }
}      
  1. 修改SplashActivity,若在安装新的apk时点击“取消”按钮,就应该回到原来的activity(SplashActivity),然后再跳转到HomeActivity,完善这个逻辑,逻辑如图所示:
  2. Android开发实战《手机安全卫士》——1.手机安全卫士的需求分析 &amp; 更新 &amp; 打包
  3. 代码如下:
package com.example.mobilesafe.activity;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.StreamUtil;
import com.example.mobilesafe.utils.ToastUtil;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class SplashActivity extends AppCompatActivity {

    /**
     * 文本控件
     */
    private TextView tv_version_name;

    /**
     * 本地版本号
     */
    private int mLocalVersionCode;

    /**
     * 更新时描述信息
     */
    private String mVersionDes;

    /**
     * 更新时的URL
     */
    private String mDownloadUrl;

    private static final String tag = "SplashActivity";

    /**
     * Handler对象
     */
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what){
                case UPDATE_VERSION:
                    // 1.弹出对话框,提示用户更新
                    showUpdateDialog();
                    break;
                case ENTER_HOME:
                    // 2.直接进入应用程序主界面
                    enterHome();
                    break;
                case URL_ERROR:
                    // 3.弹出URL错误
                    ToastUtil.show(SplashActivity.this,"url异常");
                    enterHome();
                    break;
                case IO_ERROR:
                    // 4.弹出IO错误
                    ToastUtil.show(SplashActivity.this,"IO异常");
                    enterHome();
                    break;
                case JSON_ERROR:
                    // 5.弹出JSON错误
                    ToastUtil.show(SplashActivity.this,"json异常");
                    enterHome();
                    break;
                default:break;
            }
        }
    };

    /**
     * 更新新版本的状态码
     */
    private static final int UPDATE_VERSION = 100;

    /**
     * 进入应用程序主界面的状态码
     */
    private static final int ENTER_HOME = 101;

    /**
     * URL地址出错的状态码
     */
    private static final int URL_ERROR = 102;

    /**
     * IO操作出错的状态码
     */
    private static final int IO_ERROR = 103;

    /**
     * JSON解析出错的状态码
     */
    private static final int JSON_ERROR = 104;

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

        // 初始化UI
        initUI();

        // 初始化数据
        initData();
    }

    /**
     * 1.初始化UI
     */
    private void initUI() {
        tv_version_name = findViewById(R.id.tv_version_name);
    }

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.获取应用版本名称
        String versionName = getVersionName();
        // 2.将应用版本名称设置到文本控件中
        tv_version_name.setText("版本名称:" + versionName);
        // 3.获取本地(客户端)版本号
        mLocalVersionCode = getVersionCode();
        // 4.获取服务端版本号(客户端发请求,服务端给响应(Json、Xml))
        /*
        Json中内容应该包括:
            1.更新版本的名称 versionName
            2.新版本的描述信息 versionDes
            3.服务器的版本号 versionCode
            4.新版本apk下载地址 downloadUrl
         */
        checkVersion();
    }

    /**
     * 3.获取版本应用名称(在清单文件中)
     * @return 版本名称
     */
    private String getVersionName() {
        // 1.获取包管理对象packageManager 
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本名称
            return packageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        // 4.抛出异常
        return null;
    }

    /**
     * 4.获取版本号(在清单文件中)
     * @return 版本号
     */
    private int getVersionCode() {
        // 1.获取包管理对象packageManager
        PackageManager pm = getPackageManager();
        // 2.从包管理对象中,获取指定包名的基本信息(版本名称,版本号),第二个参数传0代表获取基本信息
        try {
            PackageInfo packageInfo = pm.getPackageInfo(getPackageName(), 0);
            // 3.获取并返回版本编号
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 5.获取服务端版本号
     */
    private void checkVersion() {
        // 发送请求,获取数据,参数则为请求json的链接地址
        new Thread(){
            @Override
            public void run() {
                // 0.获取message对象
                Message msg = Message.obtain();
                long startTime = System.currentTimeMillis();// 获取时间戳
                try {
                    // 1.封装url地址
                    URL url = new URL("http://10.0.2.2:8080/update74.json");
                    // 2.开启一个链接
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    // 3.设置常见请求参数(请求头)
                    connection.setConnectTimeout(2000); // 请求超时
                    connection.setReadTimeout(2000); // 读取超时
                    //connection.setRequestMethod("GET"); // 请求方式,默认是get请求方式
                    // 4.获取相应码,200为请求成功
                    if (connection.getResponseCode() == 200){
                        // 5.以流的形式将数据获取下来
                        InputStream is = connection.getInputStream();
                        // 6.将流转换成字符串(工具封装类)
                        String json = StreamUtil.streamToString(is);
                        // 7.json解析
                        JSONObject jsonObject = new JSONObject(json);
                        String versionName = jsonObject.getString("versionName");
                        mVersionDes = jsonObject.getString("versionDes");
                        String versionCode = jsonObject.getString("versionCode");
                        mDownloadUrl = jsonObject.getString("downloadUrl");
                        // 8.比对版本号(服务器版本号 > 本地版本号,提示用户更新)
                        if (Integer.parseInt(versionCode) > mLocalVersionCode){
                            // 9.提示用户更新,弹出对话框(UI),需要使用到消息机制
                            msg.what = UPDATE_VERSION;
                        }else {
                            // 10.不需要更新,直接进入应用程序主界面
                            msg.what = ENTER_HOME;
                        }
                    }
                }catch (MalformedURLException e) {
                    e.printStackTrace();
                    msg.what = URL_ERROR;
                }catch (IOException e) {
                    e.printStackTrace();
                    msg.what = IO_ERROR;
                }
                catch (JSONException e) {
                    e.printStackTrace();
                    msg.what = JSON_ERROR;
                }finally {
                    // 11.指定睡眠时间,请求网络的时长超过4秒则不做处理,若小于4秒,则强制让其睡眠满4秒
                    long endTime = System.currentTimeMillis();
                    if (endTime - startTime < 4000){
                        try {
                            Thread.sleep(4000 - (endTime - startTime));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    // 12.发送消息
                    mHandler.sendMessage(msg);
                }
            }
        }.start();
    }

    /**
     * 6.进入应用程序的主界面
     */
    private void enterHome() {
        Intent intent = new Intent(this, HomeActivity.class);
        startActivity(intent);
        finish(); // 开启新界面后,将导航界面销毁掉
    }

    /**
     * 7.弹出更新对话框
     */
    private void showUpdateDialog() {
        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置左上角图标
        builder.setTitle("版本更新"); // 设置标题
        builder.setMessage(mVersionDes); // 设置描述内容
        builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 下载apk,需要apk的链接地址,即downloadUrl
                downloadApk();
            }
        });// 积极按钮,“是”
        builder.setNegativeButton("稍后再说", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 取消对话框,进入主界面
                enterHome();
            }
        });// 消极按钮,“否”
        builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                // 按下回退后,进入主界面,然后隐藏对话框
                enterHome();
                dialog.dismiss();
            }
        });// 回退按钮
        builder.show();
    }

    /**
     * 8.APK下载
     */
    private void downloadApk() {
        // 需要apk下载链接地址,放置apk的所在路径
        // 1.判断sd卡是否可用,是否挂载
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            // 2.获取sd卡路径
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "app-release.apk";
            Log.i(tag,"路径为:" + path);
            // 3.发送请求,获取Apk,并且放置到指定路径(下载地址,下载应用的放置位置,回调方法)
            HttpUtils httpUtils = new HttpUtils();
            httpUtils.download(mDownloadUrl, path, new RequestCallBack<File>() {
                @Override
                public void onSuccess(ResponseInfo<File> responseInfo) {
                    // 下载成功(下载过后的放置在sd卡中apk)
                    Log.i(tag,"下载成功!");
                    File file = responseInfo.result;
                    installApk(file);
                }

                @Override
                public void onFailure(HttpException e, String s) {
                    // 下载失败
                    Log.i(tag,"下载失败!");
                    e.printStackTrace();
                }

                @Override
                public void onStart() {
                    // 刚刚开始下载
                    Log.i(tag,"刚刚开始下载!");
                    super.onStart();
                }

                @Override
                public void onLoading(long total, long current, boolean isUploading) {
                    // 下载过程(下载文件大小,当前的下载位置,是否正在下载)
                    Log.i(tag,"下载中......文件大小为" + total + "当前的下载位置为" + current);
                    super.onLoading(total, current, isUploading);
                }
            });
        }
    }

    /**
     * 9.APK安装
     * @param file 安装文件
     */
    private void installApk(File file) {
        // 系统应用界面,源码,安装apk入口
        Intent intent = new Intent("android.intent.action.VIEW");
        intent.addCategory("android.intent.category.DEFAULT");
        // 文件作为数据源
        // intent.setData(Uri.fromFile(file));
        // 设置安装的类型
        // intent.setType("application/vnd.android.package-archive");
        intent.setDataAndType(Uri.fromFile(file),"application/vnd.android.package-archive");
        startActivityForResult(intent,0);
    }

    /**
     * 10.开启一个Activity后,返回结果
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        enterHome();
        super.onActivityResult(requestCode, resultCode, data);
    }
}