之前的小节中,我们完成了来电归属地显示的功能,现在就需要完成去电归属地显示的功能。
去电归属地,即主动打电话给别人,会显示别人的归属地信息。
修改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"/>
之前我们完成了“手机防盗”以及“设置中心”这两个模块的大部分功能实现,现在来完成第三个模块——通信卫士,如图中的红框所示:
在该模块中,有一个黑名单管理的功能。用户可以在这里管理黑名单,在该名单中可以添加黑名单号码,如图所示:
添加完黑名单之后的电话就无法向本机发送短信/电话,当然也可以进行删除,添加黑名单后的黑名单列表如图所示:
最后,还需要在“设置中心”里对黑名单拦截进行相关设置,如图中红框所示:
本节中主要实现黑名单的布局编写,首先修改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,代码如下:
<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>
在为黑名单功能填充数据时,我们应该先设计黑名单数据表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) {
}
}
创建好数据库后,我们来完成黑名单里数据操作的具体实现。
在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 + '\'' +
'}';
}
}
完成了数据库的创建以及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方法也是根据类似的方法进行操作,这里就不再赘述了。
我们已经将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,作为列表中单个条目的布局,代码如下:
<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,作为条目中图片的状态选择器,代码如下:
<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>
之前我们完成了黑名单列表的数据配置,现在需要完成该页面中“增加”按钮的添加黑名单号码的业务。
修改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
的布局,代码如下:
<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>
上一节中我们完成了“添加”按钮在点击时弹出的布局,接下来需要完成添加数据的逻辑。
修改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();
}
}
完成了黑名单号码的添加功能后,现在就来完成黑名单号码的删除功能。
修改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();
}
}
我们在项目中使用了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;
}
之前我们完成了黑名单功能的实现,现在需要在“设置中心”中配置黑名单配置条目,如图中红框所示:
修改SettingActivity,添加initBlackNumber(),作为初始化“黑名单拦截设置”条目的方法,同时修改对应布局,布局文件和代码分别如下:
<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(); // 展示对话框
}
}
在实现标题所示的功能之前,我们先来进行以下需求分析:
拦截短信的要求:
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.aidl
和NeighboringCellInfo.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"/>