Android开发实战《手机安全卫士》——8.“通信卫士”模块实现 & JUnit测试 & ListView优化

1.高级工具——去电归属地显示

之前的小节中,我们完成了来电归属地显示的功能,现在就需要完成去电归属地显示的功能。

去电归属地,即主动打电话给别人,会显示别人的归属地信息。

修改AddressService,添加监听打出电话的广播,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;

import androidx.annotation.NonNull;

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

public class AddressService extends Service {

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    // Layout对象
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    // 自定义的Toast布局
    private View mViewToast;

    // 获取窗体对象
    private WindowManager mWindowsManager;

    // 归属地信息
    private String mAddress;

    // 归属地信息显示控件
    private TextView tv_toast;

    // 存储资源图片id的数组
    private int[] mDrawableIds;

    // ViewToast的X坐标
    private int startX;

    // ViewToast的Y坐标
    private int startY;

    // 窗体的宽度
    private int mScreenWidth;

    // 窗体的高度
    private int mScreenHeight;

    // 监听打出电话的广播接收器
    private InnerOutCallReceiver mInnerOutCallReceiver;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            tv_toast.setText(mAddress);
        }
    };


    @Override
    public void onCreate() {
        super.onCreate();
        // 第一次开启服务时,就需要管理Toast的显示
        // 同时,还需要监听电话的状态(服务开启时监听,关闭时电话状态就不需要监听了)

        // 1.电话管理者对象
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

        mPhoneStateListener = new MyPhoneStateListener();

        // 2.监听电话状态
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);

        // 5.获取窗体对象
        mWindowsManager = (WindowManager) getSystemService(WINDOW_SERVICE);

        mScreenHeight = mWindowsManager.getDefaultDisplay().getHeight();
        mScreenWidth = mWindowsManager.getDefaultDisplay().getWidth();

        // 监听播出电话的广播过滤条件
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_NEW_OUTGOING_CALL);
        // 创建广播接受者
        mInnerOutCallReceiver = new InnerOutCallReceiver();
        registerReceiver(mInnerOutCallReceiver,intentFilter);
    }

    // 创建一个内部广播接收器
    public class InnerOutCallReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            // 接收到此广播后,需要显示自定义的Toast,显示播出归属地号码
            String phone = getResultData();
            showToast(phone);
        }
    }

    // 3.实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 4.手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态,没有任何活动,挂断电话时需要移除Toast
                    if (mWindowsManager != null && mViewToast != null){
                        mWindowsManager.removeView(mViewToast);
                    }
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态,至少有个电话活动,该活动是拨打或者通话
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态
                    showToast(phoneNumber);
                    break;
            }
        }
    }

    /**
     * 打印Toast
     */
    public void showToast(String phoneNumber) {
        // 自定义Toast
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
        params.format = PixelFormat.TRANSLUCENT;
        params.type = WindowManager.LayoutParams.TYPE_PHONE; // 在响铃的时候显示Toast,和电话类型一致
        params.gravity = Gravity.LEFT + Gravity.TOP; // 指定位置到左上角
        // 自定义了Toast的布局,需要将xml转换成view,将Toast挂到windowManager窗体上
        mViewToast = View.inflate(this, R.layout.toast_view, null);
        tv_toast = mViewToast.findViewById(R.id.tv_toast);

        mViewToast.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int moveX = (int) event.getRawX();
                        int moveY = (int) event.getRawY();

                        int disX = moveX - startX;
                        int disY = moveY - startY;

                        // 赋值给自定义控件
                        params.x = params.x + disX;
                        params.y = params.y + disY;

                        // 容错处理
                        if (params.x < 0){
                            params.x = 0;
                        }
                        if (params.y < 0){
                            params.y = 0;
                        }
                        if (params.x > mScreenWidth - mViewToast.getWidth()){
                            params.x = mScreenWidth - mViewToast.getWidth();
                        }
                        if (params.y > mScreenHeight - mViewToast.getHeight() - 22){
                            params.y = mScreenHeight - mViewToast.getHeight() - 22;

                        }

                        // 根据手势移动,在窗体上去进行自定义控件位置的更新
                        mWindowsManager.updateViewLayout(mViewToast,params);

                        // 重置一次起始坐标
                        startX = (int) event.getRawX();
                        startY = (int) event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                        SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.LOCATION_X,params.x);
                        SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.LOCATION_Y,params.y);
                        break;
                }
                // 在当前的情况下返回false表示不响应事件,返回true才表示响应事件
                // 既要响应点击事件,又要响应拖拽过程,则此返回值结果需要修改为false
                return true;
            }
        });

        // 读取sp中存储Toast左上角坐标值(x,y)
        int localX = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_X, 0);
        int localY = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.LOCATION_Y, 0);

        // 将读取的坐标值赋给params(这里的坐标默认代表左上角)
        params.x = localX;
        params.y = localY;


        // 从sp中后去色值文字的索引,匹配图片,用作展示
        mDrawableIds = new int[]{R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal,
                R.drawable.function_greenbutton_normal};
        int toastStyle = SharedPreferencesUtil.getInt(getApplicationContext(), ConstantValue.TOAST_STYLE, 0);
        tv_toast.setBackgroundResource(mDrawableIds[toastStyle]);

        mWindowsManager.addView(mViewToast,params); // 在窗体上挂载View

        // 获取了来电号码以后,需要做来电号码查询
        query(phoneNumber);
    }

    private void query(final String phoneNumber){
        new Thread(){
            @Override
            public void run() {
                mAddress = AddressDao.getAddress(phoneNumber);
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 取消对电话状态的监听
        if (mSystemService != null && mPhoneStateListener != null){
            mSystemService.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
        }
        if (mInnerOutCallReceiver != null){
            // 对广播接受者的注销
            unregisterReceiver(mInnerOutCallReceiver);
        }
    }
}

由于涉及到对打电话的操作,需要在清单文件中声明相应权限,代码如下:

<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>

2.通信卫士——黑名单布局编写

之前我们完成了“手机防盗”以及“设置中心”这两个模块的大部分功能实现,现在来完成第三个模块——通信卫士,如图中的红框所示:

在这里插入图片描述

在该模块中,有一个黑名单管理的功能。用户可以在这里管理黑名单,在该名单中可以添加黑名单号码,如图所示:

在这里插入图片描述

添加完黑名单之后的电话就无法向本机发送短信/电话,当然也可以进行删除,添加黑名单后的黑名单列表如图所示:

在这里插入图片描述

最后,还需要在“设置中心”里对黑名单拦截进行相关设置,如图中红框所示:

在这里插入图片描述

本节中主要实现黑名单的布局编写,首先修改HomeActivity中的initData()方法,添加在九宫格中进入一个模块的逻辑,代码如下:

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 1:
                        // 通信卫士
                        startActivity(new Intent(getApplicationContext(),BlackNumberActivity.class));
                        break;
                    case 7:
                        // 高级工具
                        startActivity(new Intent(getApplicationContext(),AToolActivity.class));
                        break;
                    case 8:
                        // 设置中心
                        Intent intent = new Intent(getApplicationContext(), SettingActivity.class);
                        startActivity(intent);
                        break;
                    default:
                        break;
                }
            }
        });
    }

新建一个名为BlackNumberActivity的Activity,修改其布局文件activity_black_number,代码如下:

<?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.BlackNumberActivity"
    android:orientation="vertical">

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

        <TextView
            android:gravity="left"
            android:text="黑名单管理"
            style="@style/TitleStyle"/>

        <Button
            android:id="@+id/btn_add"
            android:text="添加"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </RelativeLayout>

    <ListView
        android:id="@+id/lv_blacknumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </ListView>

</LinearLayout>

3.通信卫士——黑名单数据库

在为黑名单功能填充数据时,我们应该先设计黑名单数据表blacknumber。

该数据表blacknumber应该拥有三个字段:

字段名字段内容字段类型
_id自增长字段integer
phone黑名单号码varchar
mode拦截类型varchar

新建db包,然后在该包下新建BlackNumberOpenHelper,作为数据库Sqlite创建时的工具类,代码如下:

package com.example.mobilesafe.db;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

public class BlackNumberOpenHelper extends SQLiteOpenHelper {

    public BlackNumberOpenHelper(@Nullable Context context, @Nullable String name,@Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 创建数据库中表的方法
        db.execSQL("create table blacknumber " +
                "(_id integer primary key autoincrement , " +
                "phone varchar(20), " +
                "mode varchar(5));");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

4.通信卫士——黑名单CRUD功能实现

创建好数据库后,我们来完成黑名单里数据操作的具体实现。

在dao下新建BlackNumberDao,作为黑名单的CRUD工具类,代码如下:

package com.example.mobilesafe.dao;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.example.mobilesafe.db.BlackNumberOpenHelper;
import com.example.mobilesafe.domain.BlackNumberInfo;

import java.util.ArrayList;
import java.util.List;

public class BlackNumberDao {

    // 0.声明SQLite工具类
    private final BlackNumberOpenHelper mBlackNumberOpenHelper;

    /**
     * 让BlackNumberDao实现单例模式(懒汉)
     * 1.私有化构造方法
     * 2.声明一个当前类的对象
     * 3.提供获取单例方法,如果当前类的对象为空,创建一个新的
      */

    /**
     * 1.私有化构造方法
     * @param context 上下文环境
     */
    private BlackNumberDao(Context context) {
        // 创建数据库及其表结构
        mBlackNumberOpenHelper = new BlackNumberOpenHelper(context, "blacknumber.db", null, 1);
    }

    // 2.声明一个当前类对象
    private static BlackNumberDao blackNumberDao;

    /**
     * 3.提供获取单例方法
     * @param context 上下文环境
     * @return
     */
    public static BlackNumberDao getInstance(Context context){
        if (blackNumberDao == null){
            blackNumberDao = new BlackNumberDao(context);
        }
        return blackNumberDao;
    }

    /**
     * 4.增加一个条目
     * @param phone 拦截的电话号码
     * @param mode 拦截类型(1:短信,2:电话,3:短信 + 电话)
     */
    public void insert(String phone,String mode){
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.构建数据集
        ContentValues values = new ContentValues();
        values.put("phone",phone);
        values.put("mode",mode);
        // 3.插入数据
        db.insert("blacknumber",null,values);
        // 4.关闭数据流
        db.close();
    }

    /**
     * 5.删除一个条目
     * @param phone 待删除的条目对应的电话号码
     */
    public void delete(String phone){
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.删除数据
        db.delete("blacknumber","phone = ?",new String[]{phone});
        // 3.关闭数据流
        db.close();
    }

    /**
     * 6.修改一个条目
     * @param phone 待修改的条目对应的点好号码
     * @param mode 将要修改的拦截类型(1:短信,2:电话,3:短信 + 电话)
     */
    public void update(String phone,String mode){
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.构建数据集
        ContentValues values = new ContentValues();
        values.put("mode",mode);
        // 3.修改数据
        db.update("blacknumber",values,"phone = ?",new String[]{phone});
        // 4.关闭数据流
        db.close();
    }

    /**
     * 7.查询全部条目
     * @return 从数据库中查询到的所有的号码以及拦截类型所在的集合
     */
    public List<BlackNumberInfo> queryAll(){
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.查询数据
        Cursor cursor = db.query("blacknumber", new String[]{"phone", "mode"}, null, null, null, null, "_id desc");
        // 3.构建Java Bean集合,存储所有查询到的信息
        ArrayList<BlackNumberInfo> blackNumberList = new ArrayList<>();
        // 4.循环读取全部数据
        while (cursor.moveToNext()){
            BlackNumberInfo blackNumberInfo = new BlackNumberInfo();
            blackNumberInfo.setPhone(cursor.getString(0));
            blackNumberInfo.setMode(cursor.getString(1));
            blackNumberList.add(blackNumberInfo);
        }
        // 5.关闭游标和数据流
        cursor.close();
        db.close();
        // 6.返回数据集合
        return blackNumberList;
    }
}

为了将查询后的信息进行封装,新建一个domain包,在包下新建BlackNumberInfo作为Java Bean,BlackNumberInfo代码如下:

package com.example.mobilesafe.domain;

public class BlackNumberInfo {

    private String phone;

    private String mode;

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getMode() {
        return mode;
    }

    public void setMode(String mode) {
        this.mode = mode;
    }

    @Override
    public String toString() {
        return "BlackNumberInfo{" +
                "phone='" + phone + '\'' +
                ", mode='" + mode + '\'' +
                '}';
    }
}

5.通信卫士——JUnit测试

完成了数据库的创建以及CRUD等操作,为了测试一下是否可用,可以使用JUnit框架进行相应的测试。在Android中,JUnit被封装到了AndroidTestCase中,所以这里我们就来使用一下AndroidTestCase进行测试。

为了更好地使用AndroidTestCase,这里可以直接参考包下的ExampleInstrumentedTest类来编写测试代码,如图中的红框所示:

在这里插入图片描述

注意由于当前Android版本较高,现在已经无法继承AndroidTestCase来编写实现类,需要使用@RunWith(AndroidJUnit4.class)来进行替代,并且一些api的调用有所区别。在该包下新建名为BlackNumberDaoTest的测试类,其代码如下:

package com.example.mobilesafe;

import android.content.Context;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.example.mobilesafe.dao.BlackNumberDao;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class)
public class BlackNumberDaoTest {

    /**
     * 增加条目的测试方法
     */
    @Test
    public void insert(){
        // 1.在测试类中获取Context
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        // 2.创建BlackNumberDao实例
        BlackNumberDao dao = BlackNumberDao.getInstance(context);
        // 3.插入数据
        dao.insert("110","1");
    }
}

在逐一进行测试后可以在模拟器中找到这个数据库文件,然后查看该数据库文件,判断数据是否操作成功。这里可以用万能的DataGrip进行SQLite数据库的可视化操作。假设我们在测试类中执行了一次插入操作(insert()),然后查看数据库,发现blacknumber中已添加相应数据,如图所示:

在这里插入图片描述

其他的CRUD方法也是根据类似的方法进行操作,这里就不再赘述了。

6.通信卫士——黑名单号码数据适配器

我们已经将CRUD操作给封装好了,接下来就是实现黑名单号码列表所对应的的数据适配器了。

修改BlackNumberActivity,首先从数据库中获取数据,再通过Handler来发送消息告知ListView可以更新数据适配器了,代码如下:

package com.example.mobilesafe.activity;

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

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.BlackNumberDao;
import com.example.mobilesafe.domain.BlackNumberInfo;

import java.util.List;

public class BlackNumberActivity extends AppCompatActivity {

    private Button btn_add;

    private ListView lv_blacknumber;

    private BlackNumberDao mDao;

    private List<BlackNumberInfo> mBlackNumberList;

    private BlackNumberAdapter mAdapter;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.告知ListView可以去设置数据适配器了
            mAdapter = new BlackNumberAdapter();
            // 5.配置适配器
            lv_blacknumber.setAdapter(mAdapter);
        }
    };


    private class BlackNumberAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return mBlackNumberList.size();
        }

        @Override
        public Object getItem(int position) {
            return mBlackNumberList.get(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.listview_blacknumber_item, null);
            TextView tv_phone = view.findViewById(R.id.tv_phone);
            TextView tv_mode = view.findViewById(R.id.tv_mode);
            ImageView iv_delete = view.findViewById(R.id.iv_delete);
            tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:tv_mode.setText("拦截短信");
                    break;
                case 2:tv_mode.setText("拦截电话");
                    break;
                case 3:tv_mode.setText("拦截所有");
                    break;
            }
            return view;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_black_number);
        
        // 初始化UI
        initUI();
        
        // 初始化数据
        initData();
    }

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

    /**
     * 初始化数据
     */
    private void initData() {
        // 获取数据库中的所有电话号码
        new Thread(){
            @Override
            public void run() {
                // 1.获取操作黑名单数据库的对象
                mDao = BlackNumberDao.getInstance(getApplicationContext());
                // 2.查询所有数据
                mBlackNumberList = mDao.queryAll();
                // 3.通过消息机制告知主线程可以去使用包含数据的集合,发送空消息告知线程即可
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }
}

在res/layout下新建listview_blacknumber_item.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="match_parent">

    <TextView
        android:id="@+id/tv_phone"
        android:text="拦截号码"
        android:textSize="30sp"
        android:textColor="#000"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/tv_mode"
        android:layout_below="@id/tv_phone"
        android:text="拦截类型"
        android:textSize="30sp"
        android:textColor="#000"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <ImageView
        android:id="@+id/iv_delete"
        android:background="@drawable/selector_blacknumber_delete_btn_bg"
        android:layout_alignParentRight="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

在res/drawable下新建selector_blacknumber_delete_btn_bg.xml,作为条目中图片的状态选择器,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 选中状态:绿色垃圾桶  -->
    <item android:state_pressed="true" android:drawable="@drawable/main_clean_icon_pressed"/>
    <!-- 未选中状态:灰色垃圾桶  -->
    <item android:drawable="@drawable/main_clean_icon"/>
</selector>

7.通信卫士——黑名单号码的添加功能(布局)

之前我们完成了黑名单列表的数据配置,现在需要完成该页面中“增加”按钮的添加黑名单号码的业务。

修改BlackNumberActivity,由于进入“添加黑名单号码”后会弹出一个自定义的dialog,所以需要在其点击事件中完善相应逻辑,将其封装在showAddDialog()方法中,代码如下:

package com.example.mobilesafe.activity;

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

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.BlackNumberDao;
import com.example.mobilesafe.domain.BlackNumberInfo;

import java.util.List;

public class BlackNumberActivity extends AppCompatActivity {

    private Button btn_add;

    private ListView lv_blacknumber;

    private BlackNumberDao mDao;

    private List<BlackNumberInfo> mBlackNumberList;

    private BlackNumberAdapter mAdapter;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.告知ListView可以去设置数据适配器了
            mAdapter = new BlackNumberAdapter();
            // 5.配置适配器
            lv_blacknumber.setAdapter(mAdapter);
        }
    };


    private class BlackNumberAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return mBlackNumberList.size();
        }

        @Override
        public Object getItem(int position) {
            return mBlackNumberList.get(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.listview_blacknumber_item, null);
            TextView tv_phone = view.findViewById(R.id.tv_phone);
            TextView tv_mode = view.findViewById(R.id.tv_mode);
            ImageView iv_delete = view.findViewById(R.id.iv_delete);
            tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:tv_mode.setText("拦截短信");
                    break;
                case 2:tv_mode.setText("拦截电话");
                    break;
                case 3:tv_mode.setText("拦截所有");
                    break;
            }
            return view;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_black_number);
        
        // 初始化UI
        initUI();
        
        // 初始化数据
        initData();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        btn_add = findViewById(R.id.btn_add);
        lv_blacknumber = findViewById(R.id.lv_blacknumber);
        btn_add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showAddDialog();
            }
        });
    }

    /**
     * 初始化数据
     */
    private void initData() {
        // 获取数据库中的所有电话号码
        new Thread(){
            @Override
            public void run() {
                // 1.获取操作黑名单数据库的对象
                mDao = BlackNumberDao.getInstance(getApplicationContext());
                // 2.查询所有数据
                mBlackNumberList = mDao.queryAll();
                // 3.通过消息机制告知主线程可以去使用包含数据的集合,发送空消息告知线程即可
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    /**
     * “添加黑名单号码”的Dialog界面
     */
    private void showAddDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        AlertDialog dialog = builder.create();
        View view = View.inflate(getApplicationContext(), R.layout.dialog_add_blacknumber, null);
        dialog.setView(view,0,0,0,0);
        dialog.show();
    }
}

在res/layout下创建dialog_add_blacknumber.xml,作为自定义dialog的布局,代码如下:

<?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:background="#fcc"
        android:text="添加黑名单号码"/>

    <EditText
        android:id="@+id/et_phone"
        android:hint="请输入拦截号码"
        android:inputType="phone"
        android:textColor="#000"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <RadioGroup
        android:id="@+id/rg_group"
        android:orientation="horizontal"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <RadioButton
            android:id="@+id/rb_sms"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="true"
            android:text="短信"
            android:textColor="#000" />

        <RadioButton
            android:id="@+id/rb_phone"
            android:text="电话"
            android:textColor="#000"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <RadioButton
            android:id="@+id/rb_all"
            android:text="所有"
            android:textColor="#000"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </RadioGroup>

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

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

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

    </LinearLayout>

</LinearLayout>

8.通信卫士——黑名单号码的添加功能(逻辑)

上一节中我们完成了“添加”按钮在点击时弹出的布局,接下来需要完成添加数据的逻辑。

修改BlackNumberActivity,完善相应逻辑,代码如下:

package com.example.mobilesafe.activity;

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

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RadioGroup;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.BlackNumberDao;
import com.example.mobilesafe.domain.BlackNumberInfo;
import com.example.mobilesafe.utils.ToastUtil;

import java.util.List;

public class BlackNumberActivity extends AppCompatActivity {

    private Button btn_add;

    private ListView lv_blacknumber;

    private BlackNumberDao mDao;

    private List<BlackNumberInfo> mBlackNumberList;

    private BlackNumberAdapter mAdapter;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.告知ListView可以去设置数据适配器了
            mAdapter = new BlackNumberAdapter();
            // 5.配置适配器
            lv_blacknumber.setAdapter(mAdapter);
        }
    };

    // 默认的拦截类型
    private int mMode = 1;


    private class BlackNumberAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return mBlackNumberList.size();
        }

        @Override
        public Object getItem(int position) {
            return mBlackNumberList.get(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.listview_blacknumber_item, null);
            TextView tv_phone = view.findViewById(R.id.tv_phone);
            TextView tv_mode = view.findViewById(R.id.tv_mode);
            ImageView iv_delete = view.findViewById(R.id.iv_delete);
            tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:tv_mode.setText("拦截短信");
                    break;
                case 2:tv_mode.setText("拦截电话");
                    break;
                case 3:tv_mode.setText("拦截所有");
                    break;
            }
            return view;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_black_number);
        
        // 初始化UI
        initUI();
        
        // 初始化数据
        initData();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        btn_add = findViewById(R.id.btn_add);
        lv_blacknumber = findViewById(R.id.lv_blacknumber);
        btn_add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showAddDialog();
            }
        });
    }

    /**
     * 初始化数据
     */
    private void initData() {
        // 获取数据库中的所有电话号码
        new Thread(){
            @Override
            public void run() {
                // 1.获取操作黑名单数据库的对象
                mDao = BlackNumberDao.getInstance(getApplicationContext());
                // 2.查询所有数据
                mBlackNumberList = mDao.queryAll();
                // 3.通过消息机制告知主线程可以去使用包含数据的集合,发送空消息告知线程即可
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    /**
     * “添加黑名单号码”的Dialog界面
     */
    private void showAddDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        final AlertDialog dialog = builder.create();
        View view = View.inflate(getApplicationContext(), R.layout.dialog_add_blacknumber, null);
        dialog.setView(view,0,0,0,0);

        final EditText et_phone = view.findViewById(R.id.et_phone);
        RadioGroup rg_group = view.findViewById(R.id.rg_group);
        Button btn_submit = view.findViewById(R.id.btn_submit);
        Button btn_cancel = view.findViewById(R.id.btn_cancel);

        // 监听其选中条目的切换过程
        rg_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                switch (checkedId){
                    case R.id.rb_sms:
                        // 拦截短信
                        mMode = 1;
                        break;
                    case R.id.rb_phone:
                        // 拦截电话
                        mMode = 2;
                        break;
                    case R.id.rb_all:
                        // 拦截所有
                        mMode = 3;
                        break;

                }
            }
        });

        // “提交”按钮的点击事件
        btn_submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取输入框中的电话号码
                String phone = et_phone.getText().toString();
                if (!TextUtils.isEmpty(phone)){
                    // 2.数据库插入——当前输入的拦截电话号码
                    mDao.insert(phone,mMode + "");
                    // 3.让数据库和集合保持同步(1.数据库中的数据重新读一遍;2.手动向集合中添加对象(插入数据构建的对象))
                    BlackNumberInfo blackNumberInfo = new BlackNumberInfo();
                    blackNumberInfo.setPhone(phone);
                    blackNumberInfo.setMode(mMode + "");
                    // 4.将对象插入到集合的顶部
                    mBlackNumberList.add(0,blackNumberInfo);
                    // 5.通知数据适配器刷新(数据适配器中的集合发生改变)
                    if(mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                    // 6.关闭对话框
                    dialog.dismiss();
                }else {
                    ToastUtil.show(getApplicationContext(),"请输入拦截号码");
                }
            }
        });

        // "取消"按钮的点击事件
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 关闭对话框
                dialog.dismiss();
            }
        });

        dialog.show();
    }
}

9.通信卫士——黑名单号码的删除功能

完成了黑名单号码的添加功能后,现在就来完成黑名单号码的删除功能。

修改BlackNumberActivity,完善删除功能,代码如下:

package com.example.mobilesafe.activity;

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

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RadioGroup;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.BlackNumberDao;
import com.example.mobilesafe.domain.BlackNumberInfo;
import com.example.mobilesafe.utils.ToastUtil;

import java.util.List;

public class BlackNumberActivity extends AppCompatActivity {

    private Button btn_add;

    private ListView lv_blacknumber;

    private BlackNumberDao mDao;

    private List<BlackNumberInfo> mBlackNumberList;

    private BlackNumberAdapter mAdapter;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.告知ListView可以去设置数据适配器了
            mAdapter = new BlackNumberAdapter();
            // 5.配置适配器
            lv_blacknumber.setAdapter(mAdapter);
        }
    };

    // 默认的拦截类型
    private int mMode = 1;


    private class BlackNumberAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return mBlackNumberList.size();
        }

        @Override
        public Object getItem(int position) {
            return mBlackNumberList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            View view = View.inflate(getApplicationContext(), R.layout.listview_blacknumber_item, null);
            TextView tv_phone = view.findViewById(R.id.tv_phone);
            TextView tv_mode = view.findViewById(R.id.tv_mode);
            ImageView iv_delete = view.findViewById(R.id.iv_delete);

            // "删除"按钮的点击事件
            iv_delete.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 1.数据库中的删除
                    mDao.delete(mBlackNumberList.get(position).getPhone());
                    // 2.集合中的删除
                    mBlackNumberList.remove(position);
                    // 3.通知适配器更新
                    if (mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                }
            });

            tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:tv_mode.setText("拦截短信");
                    break;
                case 2:tv_mode.setText("拦截电话");
                    break;
                case 3:tv_mode.setText("拦截所有");
                    break;
            }
            return view;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_black_number);
        
        // 初始化UI
        initUI();
        
        // 初始化数据
        initData();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        btn_add = findViewById(R.id.btn_add);
        lv_blacknumber = findViewById(R.id.lv_blacknumber);
        btn_add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showAddDialog();
            }
        });
    }

    /**
     * 初始化数据
     */
    private void initData() {
        // 获取数据库中的所有电话号码
        new Thread(){
            @Override
            public void run() {
                // 1.获取操作黑名单数据库的对象
                mDao = BlackNumberDao.getInstance(getApplicationContext());
                // 2.查询所有数据
                mBlackNumberList = mDao.queryAll();
                // 3.通过消息机制告知主线程可以去使用包含数据的集合,发送空消息告知线程即可
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    /**
     * “添加黑名单号码”的Dialog界面
     */
    private void showAddDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        final AlertDialog dialog = builder.create();
        View view = View.inflate(getApplicationContext(), R.layout.dialog_add_blacknumber, null);
        dialog.setView(view,0,0,0,0);

        final EditText et_phone = view.findViewById(R.id.et_phone);
        RadioGroup rg_group = view.findViewById(R.id.rg_group);
        Button btn_submit = view.findViewById(R.id.btn_submit);
        Button btn_cancel = view.findViewById(R.id.btn_cancel);

        // 监听其选中条目的切换过程
        rg_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                switch (checkedId){
                    case R.id.rb_sms:
                        // 拦截短信
                        mMode = 1;
                        break;
                    case R.id.rb_phone:
                        // 拦截电话
                        mMode = 2;
                        break;
                    case R.id.rb_all:
                        // 拦截所有
                        mMode = 3;
                        break;

                }
            }
        });

        // “提交”按钮的点击事件
        btn_submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取输入框中的电话号码
                String phone = et_phone.getText().toString();
                if (!TextUtils.isEmpty(phone)){
                    // 2.数据库插入——当前输入的拦截电话号码
                    mDao.insert(phone,mMode + "");
                    // 3.让数据库和集合保持同步(1.数据库中的数据重新读一遍;2.手动向集合中添加对象(插入数据构建的对象))
                    BlackNumberInfo blackNumberInfo = new BlackNumberInfo();
                    blackNumberInfo.setPhone(phone);
                    blackNumberInfo.setMode(mMode + "");
                    // 4.将对象插入到集合的顶部
                    mBlackNumberList.add(0,blackNumberInfo);
                    // 5.通知数据适配器刷新(数据适配器中的集合发生改变)
                    if(mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                    // 6.关闭对话框
                    dialog.dismiss();
                }else {
                    ToastUtil.show(getApplicationContext(),"请输入拦截号码");
                }
            }
        });

        // "取消"按钮的点击事件
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 关闭对话框
                dialog.dismiss();
            }
        });

        dialog.show();
    }
}

10.拓展功能——ListView的优化

我们在项目中使用了ListView来以列表的形式展示数据库中的数据,并且和用户进行交互。但若有大量数据(百条数据)插入时,进行查询后并在ListView控件上浏览容易导致占用内存过大的问题,造成ANR(即主线程7s后无响应)的问题。

终其原因,是因为getView()的频繁复用,为了优化,需要使用到该方法的第二个参数,即convertView,原理如下图所示:

在这里插入图片描述

改造BlackNumberActivity中的getView(),进行相应优化(复用convertView),代码如下:

@Override
        public View getView(final int position, View convertView, ViewGroup parent) {

            View view = null;

            if (convertView == null){
                // 创建View
                view = View.inflate(getApplicationContext(), R.layout.listview_blacknumber_item, null);
            }else {
                // 复用View
                view = convertView;
            }

            TextView tv_phone = view.findViewById(R.id.tv_phone);
            TextView tv_mode = view.findViewById(R.id.tv_mode);
            ImageView iv_delete = view.findViewById(R.id.iv_delete);

            // "删除"按钮的点击事件
            iv_delete.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 1.数据库中的删除
                    mDao.delete(mBlackNumberList.get(position).getPhone());
                    // 2.集合中的删除
                    mBlackNumberList.remove(position);
                    // 3.通知适配器更新
                    if (mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                }
            });

            tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:tv_mode.setText("拦截短信");
                    break;
                case 2:tv_mode.setText("拦截电话");
                    break;
                case 3:tv_mode.setText("拦截所有");
                    break;
            }
            return view;
        }
    }

当然这种方法显得convertView有点多余,于是可以进一步优化成以下代码:

@Override
        public View getView(final int position, View convertView, ViewGroup parent) {

            if (convertView == null){
                // 创建View
                convertView = View.inflate(getApplicationContext(), R.layout.listview_blacknumber_item, null);
            }

            TextView tv_phone = convertView.findViewById(R.id.tv_phone);
            TextView tv_mode = convertView.findViewById(R.id.tv_mode);
            ImageView iv_delete = convertView.findViewById(R.id.iv_delete);

            // "删除"按钮的点击事件
            iv_delete.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 1.数据库中的删除
                    mDao.delete(mBlackNumberList.get(position).getPhone());
                    // 2.集合中的删除
                    mBlackNumberList.remove(position);
                    // 3.通知适配器更新
                    if (mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                }
            });

            tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:tv_mode.setText("拦截短信");
                    break;
                case 2:tv_mode.setText("拦截电话");
                    break;
                case 3:tv_mode.setText("拦截所有");
                    break;
            }
            return convertView;
        }
    }

其次,还可以对控件实例化时的方法findViewById进行优化,减少实例化次数,这里就需要使用到ViewHolder,结合本例其原理图如下所示:

在这里插入图片描述

进一步修改BlackNumberActivity中的getView(),整体代码如下:

    private class BlackNumberAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return mBlackNumberList.size();
        }

        @Override
        public Object getItem(int position) {
            return mBlackNumberList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {

            // 创建ViewHolder内部类
            ViewHolder holder = null;

            // (1).复用convertView
            if (convertView == null){
                // 创建View
                convertView = View.inflate(getApplicationContext(), R.layout.listview_blacknumber_item, null);
                // (2).使用ViewHolder减少findViewById次数
                holder = new ViewHolder();
                holder.tv_phone = convertView.findViewById(R.id.tv_phone);
                holder.tv_mode = convertView.findViewById(R.id.tv_mode);
                holder.iv_delete = convertView.findViewById(R.id.iv_delete);
                convertView.setTag(holder);
            }else {
                holder = (ViewHolder) convertView.getTag();
            }

            // "删除"按钮的点击事件
            holder.iv_delete.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 1.数据库中的删除
                    mDao.delete(mBlackNumberList.get(position).getPhone());
                    // 2.集合中的删除
                    mBlackNumberList.remove(position);
                    // 3.通知适配器更新
                    if (mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                }
            });

            holder.tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:holder.tv_mode.setText("拦截短信");
                    break;
                case 2:holder.tv_mode.setText("拦截电话");
                    break;
                case 3:holder.tv_mode.setText("拦截所有");
                    break;
            }
            return convertView;
        }
    }

    // (3) 将ViewHolder内部类定义成静态内部类
    private static class ViewHolder {
        // 有几个控件就有几个字段
        TextView tv_phone;
        TextView tv_mode;
        ImageView iv_delete;
    }

最后,当数据过多时,在显示时仍然会造成大量的内存压力,这里可以再一步优化:即做一个分页的算法,保证数据一次性不会显示过多。

修改BlackNumberDao,添加queryLimit(),作为查询数据时的分页查询方法,代码如下:

/**
     * 8.分页查询数据
     * @param index 索引值
     * @param page 页数
     * @return 从数据库中查询到的所有的号码以及拦截类型所在的集合
     */
    public List<BlackNumberInfo> queryLimit(int index,int page){
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.查询数据
        Cursor cursor = db.rawQuery("select * from blacknumber order by _id desc limit ?,?;",new String[]{index + "",page + ""});
        // 3.构建Java Bean集合,存储所有查询到的信息
        ArrayList<BlackNumberInfo> blackNumberList = new ArrayList<>();
        // 4.循环读取全部数据
        while (cursor.moveToNext()){
            BlackNumberInfo blackNumberInfo = new BlackNumberInfo();
            blackNumberInfo.setPhone(cursor.getString(1));
            blackNumberInfo.setMode(cursor.getString(2));
            blackNumberList.add(blackNumberInfo);
        }
        // 5.关闭游标和数据流
        cursor.close();
        db.close();
        // 6.返回数据集合
        return blackNumberList;
    }

注意,由于查询的api不同,这里对查询结果的单一字段进行取值时位置是不同的,这里需要特别注意!

另外,为了加载更多数据,还需要修改BlackNumberActivity,要满足以下条件:

  • 列表滚动到最底部,最后一个ListView的条目可见
  • 滚动状态发生改变:滚动——>停止(空闲)
  • 监听状态改变

代码如下:

package com.example.mobilesafe.activity;

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

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RadioGroup;
import android.widget.TextView;

import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.BlackNumberDao;
import com.example.mobilesafe.domain.BlackNumberInfo;
import com.example.mobilesafe.utils.ToastUtil;

import java.util.List;

public class BlackNumberActivity extends AppCompatActivity {

    private Button btn_add;

    private ListView lv_blacknumber;

    private BlackNumberDao mDao;

    private List<BlackNumberInfo> mBlackNumberList;

    private BlackNumberAdapter mAdapter;

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            // 4.告知ListView可以去设置数据适配器了
            if (mAdapter == null){
                // 这里做一个非空判断,如果为空才创建,避免重复创建
                mAdapter = new BlackNumberAdapter();
                // 5.配置适配器
                lv_blacknumber.setAdapter(mAdapter);
            }else {
                mAdapter.notifyDataSetChanged();
            }
        }
    };

    // 默认的拦截类型
    private int mMode = 1;

    // 判断是否加载的标志位
    private boolean mIsLoad = false;

    // 数据表中数据的总条数
    private int mCount;


    private class BlackNumberAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return mBlackNumberList.size();
        }

        @Override
        public Object getItem(int position) {
            return mBlackNumberList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {

            // 创建ViewHolder内部类
            ViewHolder holder = null;

            // (1).复用convertView
            if (convertView == null){
                // 创建View
                convertView = View.inflate(getApplicationContext(), R.layout.listview_blacknumber_item, null);
                // (2).使用ViewHolder减少findViewById次数
                holder = new ViewHolder();
                holder.tv_phone = convertView.findViewById(R.id.tv_phone);
                holder.tv_mode = convertView.findViewById(R.id.tv_mode);
                holder.iv_delete = convertView.findViewById(R.id.iv_delete);
                convertView.setTag(holder);
            }else {
                holder = (ViewHolder) convertView.getTag();
            }

            // "删除"按钮的点击事件
            holder.iv_delete.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 1.数据库中的删除
                    mDao.delete(mBlackNumberList.get(position).getPhone());
                    // 2.集合中的删除
                    mBlackNumberList.remove(position);
                    // 3.通知适配器更新
                    if (mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                }
            });

            holder.tv_phone.setText(mBlackNumberList.get(position).getPhone());
            // 将字符串转换成整型,便于switch-case判断
            int mode = Integer.parseInt(mBlackNumberList.get(position).getMode());
            switch (mode){
                case 1:holder.tv_mode.setText("拦截短信");
                    break;
                case 2:holder.tv_mode.setText("拦截电话");
                    break;
                case 3:holder.tv_mode.setText("拦截所有");
                    break;
            }
            return convertView;
        }
    }

    // (3) 将ViewHolder内部类定义成静态内部类
    private static class ViewHolder {
        // 有几个控件就有几个字段
        TextView tv_phone;
        TextView tv_mode;
        ImageView iv_delete;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_black_number);
        
        // 初始化UI
        initUI();
        
        // 初始化数据
        initData();
    }

    /**
     * 初始化UI
     */
    private void initUI() {
        btn_add = findViewById(R.id.btn_add);
        lv_blacknumber = findViewById(R.id.lv_blacknumber);
        btn_add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showAddDialog();
            }
        });

        // 监听其滚动状态
        lv_blacknumber.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                // 容错处理
                if (mBlackNumberList != null){
                    // 滚动过程中,状态发生改变调用方法
                    if (scrollState == SCROLL_STATE_IDLE
                            && lv_blacknumber.getLastVisiblePosition() >= mBlackNumberList.size() - 1
                            && !mIsLoad){
                        /*
                         * SCROLL_STATE_IDLE:空闲状态 & getLastVisiblePosition():列表已经滑动到底部 & mIsLoad:加载标志符
                         * mIsLoad用于防止数据重复加载。如果当前正在加载mIsLoad变为true,本次加载完毕后变成false
                         * 如果下一次加载需要执行时,会使用mIsLoad进行判断,如果为true,则需要等待上一次加载完成,将其值改为false后才能加载
                         */

                        // 条目的总数 > 集合的大小,说明还有数据,才会加载下一页数据
                        if (mCount > mBlackNumberList.size()){
                            // 加载下一页数据
                            new Thread(){
                                @Override
                                public void run() {
                                    // 1.获取操作黑名单数据库的对象
                                    mDao = BlackNumberDao.getInstance(getApplicationContext());
                                    // 2.查询分页数据
                                    List<BlackNumberInfo> moreData = mDao.queryLimit(mBlackNumberList.size(), 10);
                                    // 3.添加下一页数据(两个集合合并)
                                    mBlackNumberList.addAll(moreData);
                                    // 4.通过消息机制告知主线程可以去使用包含数据的集合,发送空消息告知线程即可
                                    mHandler.sendEmptyMessage(0);
                                }
                            }.start();
                        }
                    }
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                // 滚动过程中调用方法
            }
        });
    }

    /**
     * 初始化数据
     */
    private void initData() {
        // 获取数据库中的所有电话号码
        new Thread(){
            @Override
            public void run() {
                // 1.获取操作黑名单数据库的对象
                mDao = BlackNumberDao.getInstance(getApplicationContext());
                // 2.查询所有数据
                mBlackNumberList = mDao.queryLimit(0,10);
                mCount = mDao.getCount();
                // 3.通过消息机制告知主线程可以去使用包含数据的集合,发送空消息告知线程即可
                mHandler.sendEmptyMessage(0);
            }
        }.start();
    }

    /**
     * “添加黑名单号码”的Dialog界面
     */
    private void showAddDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        final AlertDialog dialog = builder.create();
        View view = View.inflate(getApplicationContext(), R.layout.dialog_add_blacknumber, null);
        dialog.setView(view,0,0,0,0);

        final EditText et_phone = view.findViewById(R.id.et_phone);
        RadioGroup rg_group = view.findViewById(R.id.rg_group);
        Button btn_submit = view.findViewById(R.id.btn_submit);
        Button btn_cancel = view.findViewById(R.id.btn_cancel);

        // 监听其选中条目的切换过程
        rg_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                switch (checkedId){
                    case R.id.rb_sms:
                        // 拦截短信
                        mMode = 1;
                        break;
                    case R.id.rb_phone:
                        // 拦截电话
                        mMode = 2;
                        break;
                    case R.id.rb_all:
                        // 拦截所有
                        mMode = 3;
                        break;

                }
            }
        });

        // “提交”按钮的点击事件
        btn_submit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取输入框中的电话号码
                String phone = et_phone.getText().toString();
                if (!TextUtils.isEmpty(phone)){
                    // 2.数据库插入——当前输入的拦截电话号码
                    mDao.insert(phone,mMode + "");
                    // 3.让数据库和集合保持同步(1.数据库中的数据重新读一遍;2.手动向集合中添加对象(插入数据构建的对象))
                    BlackNumberInfo blackNumberInfo = new BlackNumberInfo();
                    blackNumberInfo.setPhone(phone);
                    blackNumberInfo.setMode(mMode + "");
                    // 4.将对象插入到集合的顶部
                    mBlackNumberList.add(0,blackNumberInfo);
                    // 5.通知数据适配器刷新(数据适配器中的集合发生改变)
                    if(mAdapter != null){
                        mAdapter.notifyDataSetChanged();
                    }
                    // 6.关闭对话框
                    dialog.dismiss();
                }else {
                    ToastUtil.show(getApplicationContext(),"请输入拦截号码");
                }
            }
        });

        // "取消"按钮的点击事件
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 关闭对话框
                dialog.dismiss();
            }
        });
        dialog.show();
    }
}

最后,为了获取到数据库中数据表的数据总数,还需要在BlackNumberDao中添加一个getCount()方法,代码如下:

    /**
     * 9.获取数据表中的数据条数
     * @return 数据表中的数据总条数
     */
    public int getCount(){
        // 0.初始化数据条数
        int count = 0;
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.查询数据
        Cursor cursor = db.rawQuery("select count(*) from blacknumbe",null);
        // 3.循环读取全部数据
        if (cursor.moveToNext()){
            count = cursor.getInt(0);
        }
        // 4.关闭游标和数据流
        cursor.close();
        db.close();
        // 5.返回数据集合
        return count;
    }

11.通信卫士——开启黑名单的服务

之前我们完成了黑名单功能的实现,现在需要在“设置中心”中配置黑名单配置条目,如图中红框所示:

在这里插入图片描述

修改SettingActivity,添加initBlackNumber(),作为初始化“黑名单拦截设置”条目的方法,同时修改对应布局,布局文件和代码分别如下:

<?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="自动更新已开启"/>

    <!-- 电话归属地显示设置 -->
    <com.example.mobilesafe.view.SettingItemView
        xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
        android:id="@+id/siv_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mobilesafe:destitle="电话归属地的显示设置"
        mobilesafe:desoff="归属地的显示已关闭"
        mobilesafe:deson="归属地的显示已开启"/>

    <!-- 电话归属地显示——样式设置 -->
    <com.example.mobilesafe.view.SettingClickView
        android:id="@+id/scv_toast_style"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <!-- 电话归属地显示——位置设置 -->
    <com.example.mobilesafe.view.SettingClickView
        android:id="@+id/scv_location"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <!-- 电话归属地显示设置 -->
    <com.example.mobilesafe.view.SettingItemView
        xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
        android:id="@+id/siv_blacknumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        mobilesafe:destitle="黑名单拦截设置"
        mobilesafe:desoff="黑名单拦截已关闭"
        mobilesafe:deson="黑名单拦截已开启"/>

</LinearLayout>
package com.example.mobilesafe.activity;

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

import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import com.example.mobilesafe.R;
import com.example.mobilesafe.constant.ConstantValue;
import com.example.mobilesafe.service.AddressService;
import com.example.mobilesafe.service.BlackNumberService;
import com.example.mobilesafe.utils.ServiceUtil;
import com.example.mobilesafe.utils.SharedPreferencesUtil;
import com.example.mobilesafe.view.SettingClickView;
import com.example.mobilesafe.view.SettingItemView;

public class SettingActivity extends AppCompatActivity {

    // 描述文字所在的字符串数组
    private String[] mToastStyleDes;

    // 条目的索引值
    private int mToaststyle;

    // 自定义组合控件SettingClickView
    private SettingClickView scv_toast_style;

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

        // 初始化更新
        initUpdate();

        // 初始化显示电话号码归属地
        initAddress();

        // 初始化电话号码归属地的显示样式
        initToastStyle();

        // 初始化电话号码归属地的显示位置
        initLocation();

        // 初始化黑名单配置
        initBlackNumber();
    }

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


    /**
     * 2.初始化“显示电话号码归属地”的方法
     */
    private void initAddress() {
        final SettingItemView siv_address = findViewById(R.id.siv_address);
        // 通过ServiceUtil来判断服务是否开启
        boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.AddressService");
        siv_address.setCheck(isRunning);
        // 0.设置点击事件,切换状态(是否开启电话号码归属地)
        siv_address.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 1.获取之前的选中状态
                boolean isCheck = siv_address.isCheck();
                // 2.取反选中状态
                siv_address.setCheck(!isCheck);
                // 3.判断是否开启服务
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), AddressService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), AddressService.class));
                }
            }
        });
    }

    /**
     * 3.初始化“显示号码归属地显示样式”的方法
     */
    private void initToastStyle(){
        scv_toast_style = findViewById(R.id.scv_toast_style);
        scv_toast_style.setTitle("电话归属地样式选择");
        // 1.创建描述文字所在的String类型数组
        mToastStyleDes = new String[]{"透明", "橙色", "蓝色", "灰色", "绿色"};
        // 2.通过Sp获取Toast显示样式的索引值(int),用于描述文字
        mToaststyle = SharedPreferencesUtil.getInt(this, ConstantValue.TOAST_STYLE, 0);
        // 3.通过索引值获取字符串数组中的文字,显示给描述内容的控件上
        scv_toast_style.setDes(mToastStyleDes[mToaststyle]);
        // 4.监听点击事件,弹出对话框
        scv_toast_style.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 5.选择Toast样式的对话框
                showToastStyleDialog();
            }
        });
    }

    /**
     * 4.初始化“显示号码归属地显示位置”的方法
     */
    private void initLocation(){
        SettingClickView scv_location = findViewById(R.id.scv_location);
        scv_location.setTitle("归属地提示框的位置");
        scv_location.setDes("设置归属地提示框的位置");
        scv_location.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(getApplicationContext(),ToastLocationActivity.class));
            }
        });
    }

    /**
     * 5.初始化“黑名单是否开启”的方法
     */
    private void initBlackNumber() {
        final SettingItemView siv_blacknumber = findViewById(R.id.siv_blacknumber);
        boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.BlackNumberService");
        siv_blacknumber.setCheck(isRunning);
        siv_blacknumber.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                boolean isCheck = siv_blacknumber.isCheck();
                siv_blacknumber.setCheck(!isCheck);
                if (!isCheck){
                    // 开启服务
                    startService(new Intent(getApplicationContext(), BlackNumberService.class));
                }else {
                    // 关闭服务
                    stopService(new Intent(getApplicationContext(), BlackNumberService.class));
                }
            }
        });
    }

    /**
     * 创建选中显示样式的对话框
     */
    private void showToastStyleDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setIcon(R.drawable.ic_launcher); // 设置图标
        builder.setTitle("请选择归属地显示样式"); // 设置标题
        builder.setSingleChoiceItems(mToastStyleDes, mToaststyle, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // 1.记录选中的索引值
                SharedPreferencesUtil.putInt(getApplicationContext(),ConstantValue.TOAST_STYLE,which);
                // 2.关闭对话框
                dialog.dismiss();
                // 3.显示选中色值文字
                scv_toast_style.setDes(mToastStyleDes[which]);
            }
        }); // 单个选择条目对应的事件监听(String类型的数组,选中条目索引值,监听器)
        // “取消”按钮的点击事件监听
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        builder.show(); // 展示对话框
    }
}

12.通信卫士——在服务中拦截短信和拦截电话的功能

在实现标题所示的功能之前,我们先来进行以下需求分析:

  • 拦截短信的要求:

    • 接收短信时,会发送广播,需要创建监听广播接受者,并且拦截短信(有序);
    • 将广播接受者的优先级提高到最高级别(Integer.MaxValue)。
  • 拦截电话的要求:

    • 接收电话时,处于响铃状态,响铃状态通过代码去挂断电话,此时就拦截了电话;
    • 需要aidl进程间通信来调用api;
    • 需要反射机制来调用api。

承接之前的部分,我们新建一个名为BlackNumberService的Service,在其中首先实现拦截短信的功能,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.telephony.SmsMessage;

import com.example.mobilesafe.dao.BlackNumberDao;

public class BlackNumberService extends Service {

    private InnerSmsReceiver mInnerSmsReceiver;

    private BlackNumberDao mDao;

    @Override
    public void onCreate() {
        super.onCreate();
        // 拦截短信
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
        intentFilter.setPriority(1000); // 设置优先级
        mInnerSmsReceiver = new InnerSmsReceiver();
        registerReceiver(mInnerSmsReceiver,intentFilter);
    }

    private class InnerSmsReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 1.获取短信内容,获取发送短信的电话号码,如果此电话号码在黑名单中,并且拦截模式也为1(短信)或3(所有),拦截短信
            Object[] pdus = (Object[])intent.getExtras().get("pdus");
            // 2.循环遍历短信的过程
            for (Object object : pdus) {
                // 3.获取短信对象
                SmsMessage sms = SmsMessage.createFromPdu((byte[]) object);
                // 4.获取短信对象的基本信息
                String originatingAddress = sms.getOriginatingAddress(); // 短信地址
                String messageBody = sms.getMessageBody(); // 短信内容
                // 5.获取黑名单的数据操作类对象实例
                mDao = BlackNumberDao.getInstance(context);
                int mode = mDao.queryModeByPhone(originatingAddress);
                if (mode == 1 || mode == 3){
                    // 拦截短信,即作为优先级最高的广播接受者拦截了“接收短信”的广播,该广播是有序广播
                    abortBroadcast();
                }
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mInnerSmsReceiver != null){
            unregisterReceiver(mInnerSmsReceiver);
        }
    }


}

为了方便调用,修改BlackNumberDao,添加queryModeByPhone(),作为根据电话号码去查找拦截类型的方法,代码如下:

 /**
     * 10.根据电话号码获取拦截类型
     * @param phone 电话号码
     * @return 返回的拦截模式
     */
    public int queryModeByPhone(String phone){
        // 0.初始化拦截类型
        int mode = 0;
        // 1.开启数据库,准备进行写入操作
        SQLiteDatabase db = mBlackNumberOpenHelper.getWritableDatabase();
        // 2.查询数据
        Cursor cursor = db.query("blacknumber",new String[]{"mode"},"phone = ?",new String[]{phone},null,null,null);
        // 3.循环读取全部数据
        if (cursor.moveToNext()){
            mode = cursor.getInt(0);
        }
        // 4.关闭游标和数据流
        cursor.close();
        db.close();
        // 5.返回拦截模式
        return mode;
    }

进一步修改BlackNumberService,完善拦截电话的功能。由于挂断电话的方法放置在了aidl文件中,名称为endCall(),要调用该方法,需要去查看TelePhoneManager的源码,去查找获取ITelephony对象的方法,这里需要引入两个aidl文件:ITelephony.aidlNeighboringCellInfo.aidl,代码如下:

package com.example.mobilesafe.service;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;

import com.example.mobilesafe.dao.BlackNumberDao;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class BlackNumberService extends Service {

    private InnerSmsReceiver mInnerSmsReceiver;

    private BlackNumberDao mDao;

    private TelephonyManager mSystemService;

    private MyPhoneStateListener mPhoneStateListener;

    @Override
    public void onCreate() {
        super.onCreate();

        // 获取黑名单的数据操作类对象实例
        mDao = BlackNumberDao.getInstance(getApplicationContext());

        // 拦截短信
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
        intentFilter.setPriority(1000); // 设置优先级
        mInnerSmsReceiver = new InnerSmsReceiver();
        registerReceiver(mInnerSmsReceiver,intentFilter);

        // 拦截电话
        mSystemService = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        mPhoneStateListener = new MyPhoneStateListener();
        mSystemService.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    }

    private class InnerSmsReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 1.获取短信内容,获取发送短信的电话号码,如果此电话号码在黑名单中,并且拦截模式也为1(短信)或3(所有),拦截短信
            Object[] pdus = (Object[])intent.getExtras().get("pdus");
            // 2.循环遍历短信的过程
            for (Object object : pdus) {
                // 3.获取短信对象
                SmsMessage sms = SmsMessage.createFromPdu((byte[]) object);
                // 4.获取短信对象的基本信息
                String originatingAddress = sms.getOriginatingAddress(); // 短信地址
                String messageBody = sms.getMessageBody(); // 短信内容
                // 5.使用黑名单的数据操作类操作数据
                int mode = mDao.queryModeByPhone(originatingAddress);
                if (mode == 1 || mode == 3){
                    // 拦截短信,即作为优先级最高的广播接受者拦截了“接收短信”的广播,该广播是有序广播
                    abortBroadcast();
                }
            }
        }
    }

    // 实现一个继承了PhoneStateListener的内部类
    class MyPhoneStateListener extends PhoneStateListener{
        // 手动重写,电话状态发生改变时会触发的方法
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            super.onCallStateChanged(state, phoneNumber);
            switch (state){
                case TelephonyManager.CALL_STATE_IDLE:
                    // 空闲状态
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    // 摘机状态
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    // 响铃状态,电话关闭的api防到了aidl中
                    endCall(phoneNumber);
                    break;
            }
        }
    }

    /**
     * 挂断电话的方法
     * @param phoneNumber 要挂断的电话
     */
    private void endCall(String phoneNumber) {
        int mode = mDao.queryModeByPhone(phoneNumber);
        if (mode == 2 || mode == 3){
            // 拦截电话,由于ServiceManager此类Android对开发者隐藏,所以不能直接调用其方法,需要反射调用
            try {
                // 1.获取ServiceManger字节码文件
                Class<?> clazz = Class.forName("android.os.ServiceManager");
                // 2.获取反射方法
                Method method = clazz.getMethod("getService", String.class);
                // 3.反射调用此方法
                IBinder iBinder = (IBinder) method.invoke(null,Context.TELEPHONY_SERVICE);
                // 4.调用获取aidl文件对象方法
                ITelePhoney iTelePhoney = ITelePhoney.stub.asInterface(iBinder);
                // 5.调用在aidl中隐藏的endcall方法
                iTelePhoney.endCall();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mInnerSmsReceiver != null){
            unregisterReceiver(mInnerSmsReceiver);
        }
    }
}

由于涉及到挂断电话的操作,需要在清单文件中声明相应权限,代码如下:

<uses-permission android:name="android.permission.CALL_PHONE"/>
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

赈川

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

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

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

打赏作者

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

抵扣说明:

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

余额充值