Android开发实战《手机安全卫士》——2.“设置中心”模块实现 & 自定义组件 & Sp工具类 & MD5加密

1.home界面布局

在上一篇博客中,我们完成了SplashActivity的全部编写,现在开始需要进行主界面HomeActivity的编写,在这之前,我们还需要优化一下SplashActivity,加入一段动画

  1. 修改SplashActivity,新增initAnimation(),作为初始化动画的方法,代码如下:
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.view.animation.AlphaAnimation;
import android.widget.RelativeLayout;
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 RelativeLayout rl_root;

    /**
     * 本地版本号
     */
    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();

        // 初始化动画
        initAnimation();
    }


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

    /**
     * 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);
    }

    /**
     * 11.初始化动画
     */
    private void initAnimation() {
        AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
        alphaAnimation.setDuration(3000);
        rl_root.startAnimation(alphaAnimation);
    }
}
  1. 修改activity_splash.xml,给RelativeLayout增加id,代码如下:
<?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:id="@+id/rl_root"
    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>
  1. 为了让一些样式得到复用,我们将一些样式抽取到style.xml中,代码如下:
<resources>

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

    <style name="TitleStyle">
        <item name="android:gravity">center</item>
        <item name="android:textSize">20sp</item>
        <item name="android:textColor">#000</item>
        <item name="android:padding">10dp</item>
        <item name="android:background">#0f0</item>
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
    </style>

</resources>
  1. 开始编写HomeActivity,首先编写activity_home.xml,添加相应控件,注意这里有一个具有跑马灯效果的TextView控件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    tools:context=".activity.HomeActivity">

    <!-- 将对应属性抽取到样式当中去 -->
    <TextView
        android:text="功能列表"
        style="@style/TitleStyle"/>

    <!-- android:ellipsize="end" 添加省略点的所在位置 -->
    <!-- 跑马灯   -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
        android:textColor="#000"
        android:singleLine="true"
        android:padding="5dp"
        android:ellipsize="marquee"/>

</LinearLayout>

2.自定义获取焦点的TextView

在上一节中,我们设置了具有跑马灯效果的TextView,然而,该控件并未实现该效果,实际上是因为焦点没有进行自定义获取的原因

  1. 修改activity_home.xml,对TextView控件进行相应修改,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    tools:context=".activity.HomeActivity">

    <!-- 将对应属性抽取到样式当中去 -->
    <TextView
        android:text="功能列表"
        style="@style/TitleStyle"/>

    <!-- 想要实现跑马灯效果,需实现下面三条属性 -->
    <!--    1.android:ellipsize="end" 添加省略点的所在位置 -->
    <!--    2.android:focus="true" 获取焦点 -->
    <!--    3.android:focusableInTouchMode="true" 触摸时仍然获取焦点 -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
        android:textColor="#000"
        android:singleLine="true"
        android:padding="5dp"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:ellipsize="marquee"/>

</LinearLayout>

注意:经测试,在高版本的Android环境下,仅在xml中对原生控件进行修改已经达不到跑马灯效果了,如果想要实现这个功能的读者,可以尝试自定义View,及以下将要讲解的部分

  1. 现在想要实现一个具有跑马灯效果的自定义控件,在包下新建view包,存放自定义控件,并新建FocusTextView类,代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;

import androidx.annotation.Nullable;

/**
 * 能够获取焦点的自定义TextView
 */
public class FocusTextView extends TextView {

    /**
     * 通过在Java代码来创建控件
     * @param context 上下文
     */
    public FocusTextView(Context context) {
        super(context);
    }

    /**
     * 通过在xml代码来创建控件
     * @param context 上下文
     * @param attrs 属性
     */
    public FocusTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 通过在xml代码(结合Style)来创建控件
     * @param context 上下文
     * @param attrs 属性
     * @param defStyleAttr 样式
     */
    public FocusTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * 获取焦点的方法
     * @return
     */
    @Override
    public boolean isFocused() {
        return true;
    }
}
  1. 修改activity_home.xml,使用自定义控件FocusTextView来替换原有的TextView,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    tools:context=".activity.HomeActivity">

    <!-- 将对应属性抽取到样式当中去 -->
    <TextView
        android:text="功能列表"
        style="@style/TitleStyle"/>

    <!-- 想要实现跑马灯效果,需实现下面三条属性 -->
    <!-- 1.android:ellipsize="end" 添加省略点的所在位置
         2.android:focus="true" 获取焦点
         3.android:focusableInTouchMode="true" 触摸时仍然获取焦点
  (可选)4.android:marqueeRepeatLimit="marquee_forever" 永远滚动 -->
    <!--
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
        android:textColor="#000"
        android:singleLine="true"
        android:padding="5dp"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:ellipsize="marquee"/>
    -->
    <com.example.mobilesafe.view.FocusTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
        android:textColor="#000"
        android:singleLine="true"
        android:padding="5dp"
        android:marqueeRepeatLimit="marquee_forever"
        android:ellipsize="marquee"/>

</LinearLayout>

3.自定义控件回顾

上一节我们写了一个简单的自定义控件,这里我们来回顾一下这个过程

  1. 创建一个类,继承至TextView
  2. 重写其构造方法
  3. 重写相应的属性方法
  4. 在xml布局文件中使用(注意调用时需要全路径名称)

4.九宫格使用

由于主菜单采用九宫格样式,这里可以使用GridView来实现

  1. 修改activity_home.xml,添加gridview控件,并进行相应配置,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    tools:context=".activity.HomeActivity">

    <!-- 将对应属性抽取到样式当中去 -->
    <TextView
        android:text="功能列表"
        style="@style/TitleStyle"/>

    <!-- 想要实现跑马灯效果,需实现下面三条属性 -->
    <!-- 1.android:ellipsize="end" 添加省略点的所在位置
         2.android:focus="true" 获取焦点
         3.android:focusableInTouchMode="true" 触摸时仍然获取焦点
  (可选)4.android:marqueeRepeatLimit="marquee_forever" 永远滚动 -->
    <!--
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
        android:textColor="#000"
        android:singleLine="true"
        android:padding="5dp"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:ellipsize="marquee"/>
    -->
    <com.example.mobilesafe.view.FocusTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎使用本应用!作者赈川,欢迎共同探讨!QQ:545646733,测试测试测试测试"
        android:textColor="#000"
        android:singleLine="true"
        android:padding="5dp"
        android:marqueeRepeatLimit="marquee_forever"
        android:ellipsize="marquee"/>

    <!-- android:numColumns 指定列数 -->
    <GridView
        android:id="@+id/gv_home"
        android:numColumns="3"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
  1. 修改HomeActivity,处理GridView相关逻辑,添加适配器内部类,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class HomeActivity extends AppCompatActivity {

    /**
     * 存储标题
     */
    private String[] mTitleStrs;

    /**
     * 存储图像
     */
    private int[] mDrawableIds;

    /**
     * 网格对象
     */
    private GridView gv_home;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // 初始化UI
        initUI();

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

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

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.初始化每个图标的标题
        mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
        // 2.初始化每个图标的图像
        mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
        // 3.为GridView设置数据适配器
        gv_home.setAdapter(new MyAdapter());
    }

    /**
     * 3.自定义的数据适配器类
     */
    class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            // 统计条目的总数
            return mTitleStrs.length;
        }

        @Override
        public Object getItem(int position) {
            // 根据索引获取对象
            return mTitleStrs[position];
        }

        @Override
        public long getItemId(int position) {
            // 获取索引
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 获取视图
            View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
            TextView tv_title = view.findViewById(R.id.tv_title);
            ImageView iv_icon = view.findViewById(R.id.iv_icon);
            tv_title.setText(mTitleStrs[position]);
            iv_icon.setBackgroundResource(mDrawableIds[position]);
            return view;
        }
    }
}
  1. 在res/layout下新建gridview_item.xml,作为子项的布局,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/ic_launcher"/>

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="模块标题"/>

</LinearLayout>

5.设置中心——条目布局结构

在上一节中,我们完成了主界面的编写,这一节我们开始编写主界面中九个条目(模块)的点击事件,这里我们首先编写九宫格中最后一个条目:设置中心

  1. 修改HomeActivity,修改initData()方法,为GridView注册相应的点击事件,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class HomeActivity extends AppCompatActivity {

    /**
     * 存储标题
     */
    private String[] mTitleStrs;

    /**
     * 存储图像
     */
    private int[] mDrawableIds;

    /**
     * 网格对象
     */
    private GridView gv_home;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // 初始化UI
        initUI();

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

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

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.初始化每个图标的标题
        mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
        // 2.初始化每个图标的图像
        mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
        // 3.为GridView设置数据适配器
        gv_home.setAdapter(new MyAdapter());
        // 4.注册GridView中单个条目的点击事件
        gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                switch (position){
                    case 8:
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /**
     * 3.自定义的数据适配器类
     */
    class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            // 统计条目的总数
            return mTitleStrs.length;
        }

        @Override
        public Object getItem(int position) {
            // 根据索引获取对象
            return mTitleStrs[position];
        }

        @Override
        public long getItemId(int position) {
            // 获取索引
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 获取视图
            View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
            TextView tv_title = view.findViewById(R.id.tv_title);
            ImageView iv_icon = view.findViewById(R.id.iv_icon);
            tv_title.setText(mTitleStrs[position]);
            iv_icon.setBackgroundResource(mDrawableIds[position]);
            return view;
        }
    }
}
  1. 在activity包下新建SettingActivity,作为设置中心的活动,代码模板套用EmptyActivity即可,然后修改activity_setting.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".activity.SettingActivity"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_title"
            android:text="自动更新设置"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_des"
            android:text="自动更新已关闭"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_below="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <CheckBox
            android:id="@+id/cb_box"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <View
            android:background="#000"
            android:layout_below="@id/tv_des"
            android:layout_width="match_parent"
            android:layout_height="1dp"/>

    </RelativeLayout>

</LinearLayout>

6.设置中心——自定义组合控件构成布局结构

在上一节中我们实现了一个条目,根据需求设置界面是由许多个这样的条目组成的,为了简化开发,我们可以使用自定义组合控件来封装这些组件,形成一个整体,如图所示:

在这里插入图片描述

组合控件的核心思想是:

  • 将已经编写好的布局文件,抽取到一个类中去做管理,下次害需要使用此布局结构的时候,直接使用组合控件对应的对象即可
  • 将组合控件的布局,抽取到单独的一个xml中
  • 通过一个单独的类,去加载此段布局文件

下面开始编写代码

  1. 在res/layout目录下新建setting_item_view.xml,作为自定义组合控件的布局文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_title"
            android:text="自动更新设置"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_des"
            android:text="自动更新已关闭"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_below="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <CheckBox
            android:id="@+id/cb_box"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <View
            android:background="#000"
            android:layout_below="@id/tv_des"
            android:layout_width="match_parent"
            android:layout_height="1dp"/>

    </RelativeLayout>

</RelativeLayout>
  1. 在view下新建SettingItemView,作为组合控件的类,继承RelativeLayout,注意这里在调用inflate时第三个参数需要使用this来挂载到view上,原理如图所示:
    在这里插入图片描述

    代码如下:

package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {
    public SettingItemView(Context context) {
        this(context,null);
    }

    public SettingItemView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
        View.inflate(context,R.layout.setting_item_view,this);
        // 2.获取自定义组合控件中的每个控件的实例
        TextView tv_title = findViewById(R.id.tv_title);
        TextView tv_des = findViewById(R.id.tv_des);
        CheckBox cb_box = findViewById(R.id.cb_box);

    }
}
  1. 修改activity_setting.xml,调用刚刚编写好的自定义组合控件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".activity.SettingActivity"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="设置中心"/>

    <com.example.mobilesafe.view.SettingItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

7.设置中心——自定义组合控件中相关方法

在上一节中,我们编写了简单的自定义组合控件,这一节需要完善相关的方法

修改SettingItemView,新增isCheck()和setCheck()方法,实现在点击CheckBox时,改变TextView的文本(未选中/已选中),代码如下:

package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {

    /**
     * CheckBox
     */
    private CheckBox cb_box;

    /**
     * 文本描述控件
     */
    private TextView tv_des;

    public SettingItemView(Context context) {
        this(context,null);
    }

    public SettingItemView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
        View.inflate(context,R.layout.setting_item_view,this);
        // 2.获取自定义组合控件中的每个控件的实例
        TextView tv_title = findViewById(R.id.tv_title);
        tv_des = findViewById(R.id.tv_des);
        cb_box = findViewById(R.id.cb_box);
    }

    /**
     * 1.判断SettingItemView是否选中
     * @return SettingItemView是否选中状态
     */
    public boolean isCheck() {
        // 有CheckBox的选中结果,决定当前条目是否开启
        return cb_box.isChecked();
    }

    /**
     * 2.设置SettingItemView的选中状态
     * @param isCheck 是否作为开启的变量,由点击过程中做传递
     */
    public void setCheck(boolean isCheck){
        // 当前条目在选择的过程中,cb_box的选中状态也在跟随(isCheck)变化
        cb_box.setChecked(isCheck);
        if (isCheck){
            // 开启
            tv_des.setText("自动更新已开启");
        }else {
            // 关闭
            tv_des.setText("自动更新已关闭");
        }
    }
}

8.设置中心——选中SettingItemView条目状态切换

上一节中我们完善了自定义组合控件的一些相关方法,现在需要进行进一步地完善

  1. 修改activity_setting.xml,给自定义控件加上id,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".activity.SettingActivity"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="设置中心"/>

    <com.example.mobilesafe.view.SettingItemView
        android:id="@+id/siv_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>
  1. 修改SettingActivity,新增initUpdate(),作为修改CheckBox状态的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

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

        // 初始化更新
        initUpdate();
    }

    /**
     * 1.初始化"更新"条目的方法
     */
    private void initUpdate() {
        final SettingItemView siv_update = findViewById(R.id.siv_update);
        siv_update.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_update.isCheck();
                // 2.取反选中状态
                siv_update.setCheck(!isCheck);
            }
        });

    }
}

9.设置中心——事件传递 & 相应规则

上一节中我们完成了CheckBox的切换,但是逻辑中还存在一个涉及到事件传递的小Bug,这里我们来修复一下

先来复盘一下这个bug的产生原因:

SettingActivity对于布局文件的根布局,获取点击事件,此事件传递给SettingImageView

  • 若点击在SettingitemView非CheckBox区域,事件就由SettingImageView去做响应
  • 若点击在SettingitemViewCheckBox区域,事件就由SettingImageView传递给CheckBox,然后由CheckBox去做响应,则SettingItemView就不能再去响应此事件,则相应的点击事件则不会执行

为了解决这个问题,这里使用一个最简单粗暴的方法:禁止CheckBox可以被点击并且禁用其焦点,这样CheckBox的点击事件就无法消费,便会传回给上一级控件,也就是SettingImageView,原理图如下:

在这里插入图片描述

代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_title"
            android:text="自动更新设置"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_des"
            android:text="自动更新已关闭"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_below="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <CheckBox
            android:id="@+id/cb_box"
            android:clickable="false"
            android:focusable="false"
            android:focusableInTouchMode="false"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <View
            android:background="#000"
            android:layout_below="@id/tv_des"
            android:layout_width="match_parent"
            android:layout_height="1dp"/>

    </RelativeLayout>

</RelativeLayout>

10.设置中心——事件传递机制

在上面的这些小节中,我们大致完成了“设置中心”模块的编写,现在让我们以一张通俗易懂的图来复习一下前面因为遇到bug而面对的事件传递机制:

在这里插入图片描述

11.设置中心——sp工具类编写

解决了第一个bug,现在需要面对第二个bug:需要保存条目选中/未选中的状态,不然会导致从当前Activity回退再进来时状态的不一致,这里可以选中使用SharedPreference来实现,这里就编写一个工具类来方便以后在代码中进行相应调用

在utils包下新建SharedPreferencesUtil,作为工具类,其中主要的api和读取和写入,代码如下:

package com.example.mobilesafe.utils;

import android.content.Context;
import android.content.SharedPreferences;

public class SharedPreferencesUtil {

    private static SharedPreferences sp;

    /**
     * 1.写入(boolean)
     * @param ctx 上下文
     * @param key 键
     * @param value 值
     */
    public static void putBoolean(Context ctx,String key,boolean value){
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        sp.edit().putBoolean(key,value).commit();
    }

    /**
     * 2.读取(boolean)
     * @param ctx 上下文
     * @param key 键
     * @param defValue (默认)值
     * @return 默认值或者相应结果                
     */
    public static boolean getBoolean(Context ctx,String key,boolean defValue){
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        return sp.getBoolean(key,defValue);
    }
}

12.设置中心——sp存储更新状态

上一节中我们完成了sharedpreferences的工具类编写,现在就需要使用它来存储更新状态

  1. 修改SettingActivity,修改initUpdate()方法,完善相应逻辑,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

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

        // 初始化更新
        initUpdate();
    }

    /**
     * 1.初始化"更新"条目的方法
     */
    private void initUpdate() {
        final SettingItemView siv_update = findViewById(R.id.siv_update);
        // 0.从sp中获取已有的开关状态,然后根据这一次存储的结果去做决定
        boolean open_update = SharedPreferencesUtil.getBoolean(this, ConstantValue.OPEN_UPDATE, false);
        siv_update.setCheck(open_update);
        siv_update.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_update.isCheck();
                // 2.取反选中状态
                siv_update.setCheck(!isCheck);
                // 3.将该状态存储到sp中
                SharedPreferencesUtil.putBoolean(getApplicationContext(),ConstantValue.OPEN_UPDATE,!isCheck);
            }
        });

    }
}
  1. 在包下新建constant包,然后在包下新建ConstantValue,作为存放一些静态常量的类,方便之后调用,代码如下:
package com.example.mobilesafe.constant;

public class ConstantValue {

    /**
     * 记录更新的状态
     */
    public static final String OPEN_UPDATE = "open_update";

}

13.设置中心——使用sp来判断客户端是否需要更新

前面的小节我们使用了sp存储了更新状态,为了让该设置(即自动更新的功能)生效,这里我们再延伸一下,用于SplashActivity中的更新功能

修改SplashActivity,修改initData()方法,使用Sharepreferences(记录了更新状态,选择自动更新就会进入更新逻辑,若没有选择自动更新则直接进入主界面)来判断是否需要更新,代码如下:

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.view.animation.AlphaAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
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 RelativeLayout rl_root;

    /**
     * 本地版本号
     */
    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();

        // 初始化动画
        initAnimation();
    }


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

    /**
     * 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
         */
        if (SharedPreferencesUtil.getBoolean(this, ConstantValue.OPEN_UPDATE,false)){
            checkVersion();
        }
        else {
                // 这里不调用enterHome(),是因为直接调用会很快进入主界面(HomeActivity),跳过闪屏页面(SplashActivity)
                // 也不选择Thread.sleep(4000)后再enterHome(),是因为在主线程阻塞4秒风险太大,若到达7秒则会ANR
                // 所以选择发送消息的形式来实现在没有选择“自动更新”的情况下直接进入主界面,即时发送消息后,延时4秒钟,否则会太快进入主界面
            mHandler.sendEmptyMessageDelayed(ENTER_HOME,4000);
        }
    }

    /**
     * 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";
            String path = (SplashActivity.this).getExternalFilesDir(null) + 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);
    }

    /**
     * 11.初始化动画
     */
    private void initAnimation() {
        AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
        alphaAnimation.setDuration(3000);
        rl_root.startAnimation(alphaAnimation);
    }
}

注意:

1.在else分支中,这里不调用enterHome(),是因为直接调用会很快进入主界面(HomeActivity),跳过闪屏页面(SplashActivity)
2.也不选择Thread.sleep(4000)后再enterHome(),是因为在主线程阻塞4秒风险太大,若到达7秒则会ANR
3.所以选择发送消息的形式来实现在没有选择“自动更新”的情况下直接进入主界面,即时发送消息后,延时4秒钟,否则会太快进入主界面

14.设置中心——自定义属性申明

现在我们完成了设置中心中“自动更新”的功能,如图所示:

在这里插入图片描述

为了增加其他的功能,需要增加其他的条目(即复用之前的自定义组合控件),为了让每个条目都有所区别,这里就需要运用到一些自定义属性

参照源码,在res/value下新建attrs.xml,存放一些用到的自定义属性,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="com.example.mobilesafe.view.SettingItemView">
        <!-- 标题内容 -->
        <attr name="destitle" format="string"/>
        <!-- 单选框关闭时的文本内容 -->
        <attr name="desoff" format="string"/>
        <!-- 单选框开启时的文本内容 -->
        <attr name="deson" format="string"/>
    </declare-styleable>
</resources>

15.设置中心——构造方法中获取自定义属性值

上一节中我们申明了自定义属性,这一节需要我们在构造方法中使用这些属性,大概分为这几步:

  • 定义名空间namespace:类似于xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
  • 在控件中调用相关属性:类似于mobilesafe:destitle="自动更新设置"
  • 需要在view类中获取相应属性值

现在开始编写代码

  1. 修改activity_setting.xml,调用自定义属性,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".activity.SettingActivity"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="设置中心"/>

    <!-- 自动更新 -->
    <com.example.mobilesafe.view.SettingItemView
        xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
        android:id="@+id/siv_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mobilesafe:destitle="自动更新设置"
        mobilesafe:desoff="自动更新已关闭"
        mobilesafe:deson="自动更新已开启"/>

</LinearLayout>
  1. 修改SettingItemView,添加initAttrs()方法,用于修改自定义属性集合,代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {

    /**
     * CheckBox
     */
    private CheckBox cb_box;

    /**
     * 文本描述控件
     */
    private TextView tv_des;

    public SettingItemView(Context context) {
        this(context,null);
    }

    public SettingItemView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
        View.inflate(context,R.layout.setting_item_view,this);
        // 2.获取自定义组合控件中的每个控件的实例
        TextView tv_title = findViewById(R.id.tv_title);
        tv_des = findViewById(R.id.tv_des);
        cb_box = findViewById(R.id.cb_box);
        // 3.获取自定义以及原生属性,从AttributeSet attrs参数中获取
        initAttrs(attrs);
    }

    /**
     * 1.判断SettingItemView是否选中
     * @return SettingItemView是否选中状态
     */
    public boolean isCheck() {
        // 有CheckBox的选中结果,决定当前条目是否开启
        return cb_box.isChecked();
    }

    /**
     * 2.设置SettingItemView的选中状态
     * @param isCheck 是否作为开启的变量,由点击过程中做传递
     */
    public void setCheck(boolean isCheck){
        // 当前条目在选择的过程中,cb_box的选中状态也在跟随(isCheck)变化
        cb_box.setChecked(isCheck);
        if (isCheck){
            // 开启
            tv_des.setText("自动更新已开启");
        }else {
            // 关闭
            tv_des.setText("自动更新已关闭");
        }
    }

    /**
     * 3.初始化属性
     * @param attrs 维护好的属性集合
     */
    private void initAttrs(AttributeSet attrs) {
    }
}

16.设置中心——给自定义组合控件内部控件赋值

上一节中我们定义了initAttrs()方法,现在来完善它的具体实现

  1. 修改SettingItemView,修改initAttrs()方法,完善相应逻辑,代码如下:
package com.example.mobilesafe.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.example.mobilesafe.R;

public class SettingItemView extends RelativeLayout {

    /**
     * 自定义的命名空间
     */
    private static final String NAMESPACE = "http://schemas.android.com/apk/res/com.example.mobilesafe";

    /**
     * CheckBox
     */
    private CheckBox cb_box;

    /**
     * 文本描述控件
     */
    private TextView tv_des;

    /**
     * 自定义命名空间:标题
     */
    private String mDestitle;

    /**
     * 自定义命名空间:关闭按钮后文本
     */
    private String mDesoff;

    /**
     * 自定义命名空间:开启按钮后文本
     */
    private String mDeson;

    public SettingItemView(Context context) {
        this(context,null);
    }

    public SettingItemView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SettingItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 1.将xml转换为view,即将设置界面的一个条目转换成view对象,并添加到了当前的类中
        View.inflate(context,R.layout.setting_item_view,this);
        // 2.获取自定义组合控件中的每个控件的实例
        TextView tv_title = findViewById(R.id.tv_title);
        tv_des = findViewById(R.id.tv_des);
        cb_box = findViewById(R.id.cb_box);
        // 3.获取自定义以及原生属性,从AttributeSet attrs参数中获取
        initAttrs(attrs);
        // 4.为控件赋值
        tv_title.setText(mDestitle);

    }

    /**
     * 1.判断SettingItemView是否选中
     * @return SettingItemView是否选中状态
     */
    public boolean isCheck() {
        // 有CheckBox的选中结果,决定当前条目是否开启
        return cb_box.isChecked();
    }

    /**
     * 2.设置SettingItemView的选中状态
     * @param isCheck 是否作为开启的变量,由点击过程中做传递
     */
    public void setCheck(boolean isCheck){
        // 当前条目在选择的过程中,cb_box的选中状态也在跟随(isCheck)变化
        cb_box.setChecked(isCheck);
        if (isCheck){
            // 开启
            tv_des.setText(mDeson);
        }else {
            // 关闭
            tv_des.setText(mDesoff);
        }
    }

    /**
     * 3.初始化属性
     * @param attrs 维护好的属性集合
     */
    private void initAttrs(AttributeSet attrs) {
        mDestitle = attrs.getAttributeValue(NAMESPACE, "destitle");
        mDesoff = attrs.getAttributeValue(NAMESPACE, "desoff");
        mDeson = attrs.getAttributeValue(NAMESPACE, "deson");
    }
}
  1. 修改setting_item_view.xml,将写死的文本删除,便于之后代码进行复用,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_title"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_des"
            android:textColor="#000"
            android:textSize="18sp"
            android:layout_below="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <CheckBox
            android:id="@+id/cb_box"
            android:clickable="false"
            android:focusable="false"
            android:focusableInTouchMode="false"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <View
            android:background="#000"
            android:layout_below="@id/tv_des"
            android:layout_width="match_parent"
            android:layout_height="1dp"/>

    </RelativeLayout>

</RelativeLayout>

17.手机防盗——是否有密码区分对话框类型

设置中心的第一个功能——自动更新可以先告一段落了,现在我们先编写九宫格中的第二个模块——手机防盗,其中的一个功能就是设置密码,如图所示:

在这里插入图片描述

在首次使用该功能时,需要设置一个初始密码,之后才能进入,后面再从主界面进入时,需要输入初始密码,才能使用该功能,如图所示:

在这里插入图片描述

现在我们先来实现这个对话框的逻辑

  1. 修改SharedPreferencesUtil,添加读写字符串的方法,用作存储密码,代码如下:
package com.example.mobilesafe.utils;

import android.content.Context;
import android.content.SharedPreferences;

public class SharedPreferencesUtil {

    private static SharedPreferences sp;

    /**
     * 1.写入(boolean)
     * @param ctx 上下文
     * @param key 键
     * @param value 值
     */
    public static void putBoolean(Context ctx,String key,boolean value){
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        sp.edit().putBoolean(key,value).commit();
    }

    /**
     * 2.读取(boolean)
     * @param ctx 上下文
     * @param key 键
     * @param defValue (默认)值
     * @return 默认值或者相应结果
     */
    public static boolean getBoolean(Context ctx,String key,boolean defValue){
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        return sp.getBoolean(key,defValue);
    }

    /**
     * 3.写入(string)
     * @param ctx 上下文
     * @param key 键
     * @param value 值
     */
    public static void putString(Context ctx,String key,String value){
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        sp.edit().putString(key,value).commit();
    }

    /**
     * 4.读取(string)
     * @param ctx 上下文
     * @param key 键
     * @param defValue (默认)值
     * @return 默认值或者相应结果
     */
    public static String getString(Context ctx,String key,String defValue){
        if (sp == null){
            sp = ctx.getSharedPreferences("config", Context.MODE_PRIVATE);
        }
        return sp.getString(key,defValue);
    }
}

  1. 修改ConstantValue,新增一个代表密码的标识符,代码如下:
package com.example.mobilesafe.constant;

public class ConstantValue {

    /**
     * 记录更新的状态
     */
    public static final String OPEN_UPDATE = "open_update";

    /**
     * 手机防盗——设置密码的状态
     */
    public static final String MOBILE_SAFE_PASSWORD = "mobile_safe_password";

}
  1. 修改HomeActivity,添加showDialog(),作为弹出密码编辑的对话框的方法,代码如下:
package com.example.mobilesafe.activity;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class HomeActivity extends AppCompatActivity {

    /**
     * 存储标题
     */
    private String[] mTitleStrs;

    /**
     * 存储图像
     */
    private int[] mDrawableIds;

    /**
     * 网格对象
     */
    private GridView gv_home;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // 初始化UI
        initUI();

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

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

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.初始化每个图标的标题
        mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
        // 2.初始化每个图标的图像
        mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
        // 3.为GridView设置数据适配器
        gv_home.setAdapter(new MyAdapter());
        // 4.注册GridView中单个条目的点击事件
        gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                switch (position){
                    case 0:
                        // 手机防盗
                        showDialog();
                    case 8:
                        // 设置中心
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /**
     * 3.自定义的数据适配器类
     */
    class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            // 统计条目的总数
            return mTitleStrs.length;
        }

        @Override
        public Object getItem(int position) {
            // 根据索引获取对象
            return mTitleStrs[position];
        }

        @Override
        public long getItemId(int position) {
            // 获取索引
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 获取视图
            View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
            TextView tv_title = view.findViewById(R.id.tv_title);
            ImageView iv_icon = view.findViewById(R.id.iv_icon);
            tv_title.setText(mTitleStrs[position]);
            iv_icon.setBackgroundResource(mDrawableIds[position]);
            return view;
        }
    }

    /**
     * 4.手机防盗——密码对话框
     */
    private void showDialog() {
        // 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
        String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
        if (TextUtils.isEmpty(password)){
            // 2.初始设置密码对话框
            showSetPasswordDialog();
        }else {
            // 3.确认密码对话框
            showConfirmPasswordDialog();
        }
    }

    /**
     * 5.初次设置密码对话框
     */
    private void showSetPasswordDialog() {
    }

    /**
     * 6.再次设置密码对话框
     */
    private void showConfirmPasswordDialog() {
    }
}

18.手机防盗——设置密码对话框

上一节中我们完成了密码对话框的骨架,现在来进一步完善编写的两个方法

  1. 修改HomeActivity,完善showSetPasswordDialog()方法,代码如下:
package com.example.mobilesafe.activity;

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

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;

public class HomeActivity extends AppCompatActivity {

    /**
     * 存储标题
     */
    private String[] mTitleStrs;

    /**
     * 存储图像
     */
    private int[] mDrawableIds;

    /**
     * 网格对象
     */
    private GridView gv_home;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // 初始化UI
        initUI();

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

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

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.初始化每个图标的标题
        mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
        // 2.初始化每个图标的图像
        mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
        // 3.为GridView设置数据适配器
        gv_home.setAdapter(new MyAdapter());
        // 4.注册GridView中单个条目的点击事件
        gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                switch (position){
                    case 0:
                        // 手机防盗
                        showDialog();
                        break;
                    case 8:
                        // 设置中心
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /**
     * 3.自定义的数据适配器类
     */
    class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            // 统计条目的总数
            return mTitleStrs.length;
        }

        @Override
        public Object getItem(int position) {
            // 根据索引获取对象
            return mTitleStrs[position];
        }

        @Override
        public long getItemId(int position) {
            // 获取索引
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 获取视图
            View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
            TextView tv_title = view.findViewById(R.id.tv_title);
            ImageView iv_icon = view.findViewById(R.id.iv_icon);
            tv_title.setText(mTitleStrs[position]);
            iv_icon.setBackgroundResource(mDrawableIds[position]);
            return view;
        }
    }

    /**
     * 4.手机防盗——密码对话框
     */
    private void showDialog() {
        // 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
        String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
        if (TextUtils.isEmpty(password)){
            // 2.初始设置密码对话框
            showSetPasswordDialog();
        }else {
            // 3.确认密码对话框
            showConfirmPasswordDialog();
        }
    }

    /**
     * 5.初次设置密码对话框
     */
    private void showSetPasswordDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        AlertDialog dialog = builder.create();
        View view = View.inflate(this, R.layout.dialog_set_password, null);
        dialog.setView(view);
        dialog.show();
    }

    /**
     * 6.再次设置密码对话框
     */
    private void showConfirmPasswordDialog() {
    }
}

  1. 在res/layout下新建dialog_set_password,作为初始化密码的对话框的布局,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="设置密码"
        android:background="#f00"/>

    <EditText
        android:id="@+id/et_set_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="设置密码"/>

    <EditText
        android:id="@+id/et_confirm_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="确认密码"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/btn_submit"
            android:text="确认"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn_cancel"
            android:text="取消"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>

    </LinearLayout>

</LinearLayout>

19.手机防盗——对话初次设置密码验证过程

上一节中我们完成了初次设置密码的对话框的布局,现在需要完善相应的逻辑

  1. 修改HomeActivity,修改showSetPasswordDialog()方法,新建一个套用Empty Activity模板的TestActivity作为测试Activity,代码如下:
package com.example.mobilesafe.activity;

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

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.utils.ToastUtil;

public class HomeActivity extends AppCompatActivity {

    /**
     * 存储标题
     */
    private String[] mTitleStrs;

    /**
     * 存储图像
     */
    private int[] mDrawableIds;

    /**
     * 网格对象
     */
    private GridView gv_home;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // 初始化UI
        initUI();

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

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

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.初始化每个图标的标题
        mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
        // 2.初始化每个图标的图像
        mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
        // 3.为GridView设置数据适配器
        gv_home.setAdapter(new MyAdapter());
        // 4.注册GridView中单个条目的点击事件
        gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                switch (position){
                    case 0:
                        // 手机防盗
                        showDialog();
                        break;
                    case 8:
                        // 设置中心
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /**
     * 3.自定义的数据适配器类
     */
    class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            // 统计条目的总数
            return mTitleStrs.length;
        }

        @Override
        public Object getItem(int position) {
            // 根据索引获取对象
            return mTitleStrs[position];
        }

        @Override
        public long getItemId(int position) {
            // 获取索引
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 获取视图
            View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
            TextView tv_title = view.findViewById(R.id.tv_title);
            ImageView iv_icon = view.findViewById(R.id.iv_icon);
            tv_title.setText(mTitleStrs[position]);
            iv_icon.setBackgroundResource(mDrawableIds[position]);
            return view;
        }
    }

    /**
     * 4.手机防盗——密码对话框
     */
    private void showDialog() {
        // 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
        String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
        if (TextUtils.isEmpty(password)){
            // 2.初始设置密码对话框
            showSetPasswordDialog();
        }else {
            // 3.确认密码对话框
            showConfirmPasswordDialog();
        }
    }

    /**
     * 5.初次设置密码对话框
     */
    private void showSetPasswordDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        final AlertDialog dialog = builder.create();
        final View view = View.inflate(this, R.layout.dialog_set_password, null);
        dialog.setView(view);
        dialog.show();
        Button btn_submit = view.findViewById(R.id.btn_submit);
        Button btn_cancel = view.findViewById(R.id.btn_cancel);
        btn_submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText et_set_password = view.findViewById(R.id.et_set_password);
                EditText et_confirm_password = view.findViewById(R.id.et_confirm_password);
                String password = et_set_password.getText().toString();
                String confirmpassword = et_confirm_password.getText().toString();
                if (!TextUtils.isEmpty(password) && !TextUtils.isEmpty(confirmpassword)){
                    if(password.equals(confirmpassword)){
                        // 进入手机防盗模块
                        Intent intent = new Intent(getApplicationContext(), TestActivity.class);
                        startActivity(intent);
                        dialog.dismiss();
                    }else {
                        // 提示用户确认密码有误
                        ToastUtil.show(getApplicationContext(),"确认密码错误");
                    }
                }else {
                    // 提示用户密码输入有为空
                    ToastUtil.show(getApplicationContext(),"请输入密码");
                }
            }
        });
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dialog.dismiss();
            }
        });
    }

    /**
     * 6.再次设置密码对话框
     */
    private void showConfirmPasswordDialog() {
    }
}
  1. 修改dialog_set_password.xml,为编辑框添加密码属性,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="设置密码"
        android:background="#f00"/>

    <EditText
        android:id="@+id/et_set_password"
        android:inputType="textPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="设置密码"/>

    <EditText
        android:id="@+id/et_confirm_password"
        android:inputType="textPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="确认密码"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/btn_submit"
            android:text="确认"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn_cancel"
            android:text="取消"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>

    </LinearLayout>

</LinearLayout>

20.手机防盗——确认密码对话框编写

前面我们完成了初次设置密码对话框的逻辑,现在需要编写确认密码对话框的逻辑

  1. 在res/layout下新建dialog_confirm_password.xml,作为确认密码对话框的视图,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        style="@style/TitleStyle"
        android:text="确认密码"
        android:background="#f00"/>

    <EditText
        android:id="@+id/et_confirm_password"
        android:inputType="textPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="确认密码"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/btn_submit"
            android:text="确认"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>

        <Button
            android:id="@+id/btn_cancel"
            android:text="取消"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"/>

    </LinearLayout>

</LinearLayout>
  1. 修改HomeActivity,修改showConfirmPasswordDialog()方法,完善相应逻辑,代码如下:
package com.example.mobilesafe.activity;

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

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.utils.ToastUtil;

public class HomeActivity extends AppCompatActivity {

    /**
     * 存储标题
     */
    private String[] mTitleStrs;

    /**
     * 存储图像
     */
    private int[] mDrawableIds;

    /**
     * 网格对象
     */
    private GridView gv_home;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // 初始化UI
        initUI();

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

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

    /**
     * 2.初始化数据
     */
    private void initData() {
        // 1.初始化每个图标的标题
        mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
        // 2.初始化每个图标的图像
        mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
        // 3.为GridView设置数据适配器
        gv_home.setAdapter(new MyAdapter());
        // 4.注册GridView中单个条目的点击事件
        gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                switch (position){
                    case 0:
                        // 手机防盗
                        showDialog();
                        break;
                    case 8:
                        // 设置中心
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /**
     * 3.自定义的数据适配器类
     */
    class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            // 统计条目的总数
            return mTitleStrs.length;
        }

        @Override
        public Object getItem(int position) {
            // 根据索引获取对象
            return mTitleStrs[position];
        }

        @Override
        public long getItemId(int position) {
            // 获取索引
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 获取视图
            View view = View.inflate(getApplicationContext(), R.layout.gridview_item, null);
            TextView tv_title = view.findViewById(R.id.tv_title);
            ImageView iv_icon = view.findViewById(R.id.iv_icon);
            tv_title.setText(mTitleStrs[position]);
            iv_icon.setBackgroundResource(mDrawableIds[position]);
            return view;
        }
    }

    /**
     * 4.手机防盗——密码对话框
     */
    private void showDialog() {
        // 1.通过判断本地是否有存储密码来确定显示哪个对话框(sp)
        String password = SharedPreferencesUtil.getString(this, ConstantValue.MOBILE_SAFE_PASSWORD, "");
        if (TextUtils.isEmpty(password)){
            // 2.初始设置密码对话框
            showSetPasswordDialog();
        }else {
            // 3.确认密码对话框
            showConfirmPasswordDialog();
        }
    }

    /**
     * 5.初次设置密码对话框
     */
    private void showSetPasswordDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        final AlertDialog dialog = builder.create();
        final View view = View.inflate(this, R.layout.dialog_set_password, null);
        dialog.setView(view);
        dialog.show();
        Button btn_submit = view.findViewById(R.id.btn_submit);
        Button btn_cancel = view.findViewById(R.id.btn_cancel);
        btn_submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText et_set_password = view.findViewById(R.id.et_set_password);
                EditText et_confirm_password = view.findViewById(R.id.et_confirm_password);
                String password = et_set_password.getText().toString();
                String confirmpassword = et_confirm_password.getText().toString();
                if (!TextUtils.isEmpty(password) && !TextUtils.isEmpty(confirmpassword)){
                    if(password.equals(confirmpassword)){
                        // 进入手机防盗模块
                        Intent intent = new Intent(getApplicationContext(), TestActivity.class);
                        startActivity(intent);
                        dialog.dismiss();
                        // 将密码存储到sp中
                        SharedPreferencesUtil.putString(getApplicationContext(),ConstantValue.MOBILE_SAFE_PASSWORD,password);
                    }else {
                        // 提示用户确认密码有误
                        ToastUtil.show(getApplicationContext(),"确认密码错误");
                    }
                }else {
                    // 提示用户密码输入有为空
                    ToastUtil.show(getApplicationContext(),"请输入密码");
                }
            }
        });
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dialog.dismiss();
            }
        });
    }

    /**
     * 6.再次设置密码对话框
     */
    private void showConfirmPasswordDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        final AlertDialog dialog = builder.create();
        final View view = View.inflate(this, R.layout.dialog_confirm_password, null);
        dialog.setView(view);
        dialog.show();
        Button btn_submit = view.findViewById(R.id.btn_submit);
        Button btn_cancel = view.findViewById(R.id.btn_cancel);
        btn_submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText et_confirm_password = view.findViewById(R.id.et_confirm_password);
                String confirmpassword = et_confirm_password.getText().toString();
                if (!TextUtils.isEmpty(confirmpassword)){
                    // 从sp中获取密码
                    String password = SharedPreferencesUtil.getString(getApplicationContext(), ConstantValue.MOBILE_SAFE_PASSWORD, "");
                    if(password.equals(confirmpassword)){
                        // 进入手机防盗模块
                        Intent intent = new Intent(getApplicationContext(), TestActivity.class);
                        startActivity(intent);
                        dialog.dismiss();
                    }else {
                        // 提示用户确认密码有误
                        ToastUtil.show(getApplicationContext(),"确认密码错误");
                    }
                }else {
                    // 提示用户密码输入有为空
                    ToastUtil.show(getApplicationContext(),"请输入密码");
                }
            }
        });
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dialog.dismiss();
            }
        });
    }
}

21.手机防盗——MD5加密过程

之前的小节中,我们完成了密码的设置和存储,但是密码是用明文存储在sp中的,为了应用安全,所以需要使用MD5加密算法进行一个加密

MD5:将字符串转换成32位的字符串(16进制),不可逆

在utils包下新建MD5Util,作为MD5加密算法的工具类,代码如下:

package com.example.mobilesafe.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

    /**
     * 给指定字符串按照MD5算法进行加密
     * @param password 待加密的字符型
     */
    public static void encoder(String password){
        try {
            // 1.指定加密算法类型
            MessageDigest digest = MessageDigest.getInstance("MD5");
            // 2.将需要加密的字符串中转换成byte类型的数组,然后进行随机的哈希过程
            byte[] bs = digest.digest(password.getBytes());
            // 3.循环遍历数组,然后让其生成32位字符串,固定写法
            StringBuffer stringBuffer = new StringBuffer();
            for (byte b : bs) {
                int i = b & 0xff;
                // 4.将int类型的i转换成16进制字符
                String hexString = Integer.toHexString(i);
                if (hexString.length() < 2){
                    hexString = "0" + hexString;
                }
                // 5.字符串拼接
                stringBuffer.append(hexString);
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值