文章目录
1.高级工具——去电归属地显示2.通信卫士——黑名单布局编写3.通信卫士——黑名单数据库4.通信卫士——黑名单CRUD功能实现5.通信卫士——JUnit测试6.通信卫士——黑名单号码数据适配器7.通信卫士——黑名单号码的添加功能(布局)8.通信卫士——黑名单号码的添加功能(逻辑)9.通信卫士——黑名单号码的删除功能10.拓展功能——ListView的优化11.通信卫士——开启黑名单的服务12.通信卫士——在服务中拦截短信和拦截电话的功能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(){@Overridepublic void handleMessage(@NonNull Message msg) {tv_toast.setText(mAddress);}};@Overridepublic 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 {@Overridepublic void onReceive(Context context, Intent intent) {// 接收到此广播后,需要显示自定义的Toast,显示播出归属地号码String phone = getResultData();showToast(phone);}}// 3.实现一个继承了PhoneStateListener的内部类class MyPhoneStateListener extends PhoneStateListener{// 4.手动重写,电话状态发生改变时会触发的方法@Overridepublic void onCallStateChanged(int state, String phoneNumber) {super.onCallStateChanged(state, phoneNumber);switch (state){case TelephonyManager.CALL_STATE_IDLE:// 空闲状态,没有任何活动,挂断电话时需要移除Toastif (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) {// 自定义Toastfinal 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() {@Overridepublic 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才表示响应事件// 既要响应点击事件,又要响应拖拽过程,则此返回值结果需要修改为falsereturn 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(){@Overridepublic void run() {mAddress = AddressDao.getAddress(phoneNumber);mHandler.sendEmptyMessage(0);}}.start();}@Overridepublic IBinder onBind(Intent intent) {throw new UnsupportedOperationException("Not yet implemented");}@Overridepublic 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() {@Overridepublic 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"?><LinearLayoutxmlns:android="/apk/res/android"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".activity.BlackNumberActivity"android:orientation="vertical"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid:gravity="left"android:text="黑名单管理"style="@style/TitleStyle"/><Buttonandroid: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><ListViewandroid:id="@+id/lv_blacknumber"android:layout_width="match_parent"android:layout_height="wrap_content"></ListView></LinearLayout>
3.通信卫士——黑名单数据库
在为黑名单功能填充数据时,我们应该先设计黑名单数据表blacknumber。
该数据表blacknumber应该拥有三个字段:
新建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);}@Overridepublic void onCreate(SQLiteDatabase db) {// 创建数据库中表的方法db.execSQL("create table blacknumber " +"(_id integer primary key autoincrement , " +"phone varchar(20), " +"mode varchar(5));");}@Overridepublic 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;}@Overridepublic 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 {/*** 增加条目的测试方法*/@Testpublic void insert(){// 1.在测试类中获取ContextContext 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(){@Overridepublic void handleMessage(@NonNull Message msg) {// 4.告知ListView可以去设置数据适配器了mAdapter = new BlackNumberAdapter();// 5.配置适配器lv_blacknumber.setAdapter(mAdapter);}};private class BlackNumberAdapter extends BaseAdapter {@Overridepublic int getCount() {return mBlackNumberList.size();}@Overridepublic Object getItem(int position) {return mBlackNumberList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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;}}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_black_number);// 初始化UIinitUI();// 初始化数据initData();}/*** 初始化UI*/private void initUI() {btn_add = findViewById(R.id.btn_add);lv_blacknumber = findViewById(R.id.lv_blacknumber);}/*** 初始化数据*/private void initData() {// 获取数据库中的所有电话号码new Thread(){@Overridepublic 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"?><RelativeLayoutxmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv_phone"android:text="拦截号码"android:textSize="30sp"android:textColor="#000"android:layout_width="wrap_content"android:layout_height="wrap_content"/><TextViewandroid: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"/><ImageViewandroid: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="/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(){@Overridepublic void handleMessage(@NonNull Message msg) {// 4.告知ListView可以去设置数据适配器了mAdapter = new BlackNumberAdapter();// 5.配置适配器lv_blacknumber.setAdapter(mAdapter);}};private class BlackNumberAdapter extends BaseAdapter {@Overridepublic int getCount() {return mBlackNumberList.size();}@Overridepublic Object getItem(int position) {return mBlackNumberList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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;}}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_black_number);// 初始化UIinitUI();// 初始化数据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() {@Overridepublic void onClick(View v) {showAddDialog();}});}/*** 初始化数据*/private void initData() {// 获取数据库中的所有电话号码new Thread(){@Overridepublic 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"?><LinearLayoutxmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><TextViewstyle="@style/TitleStyle"android:background="#fcc"android:text="添加黑名单号码"/><EditTextandroid:id="@+id/et_phone"android:hint="请输入拦截号码"android:inputType="phone"android:textColor="#000"android:layout_width="match_parent"android:layout_height="wrap_content"/><RadioGroupandroid:id="@+id/rg_group"android:orientation="horizontal"android:gravity="center"android:layout_width="match_parent"android:layout_height="wrap_content"><RadioButtonandroid:id="@+id/rb_sms"android:layout_width="wrap_content"android:layout_height="wrap_content"android:checked="true"android:text="短信"android:textColor="#000" /><RadioButtonandroid:id="@+id/rb_phone"android:text="电话"android:textColor="#000"android:layout_width="wrap_content"android:layout_height="wrap_content"/><RadioButtonandroid:id="@+id/rb_all"android:text="所有"android:textColor="#000"android:layout_width="wrap_content"android:layout_height="wrap_content"/></RadioGroup><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"><Buttonandroid:id="@+id/btn_submit"android:text="确认"android:textColor="#000"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1" /><Buttonandroid: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(){@Overridepublic void handleMessage(@NonNull Message msg) {// 4.告知ListView可以去设置数据适配器了mAdapter = new BlackNumberAdapter();// 5.配置适配器lv_blacknumber.setAdapter(mAdapter);}};// 默认的拦截类型private int mMode = 1;private class BlackNumberAdapter extends BaseAdapter {@Overridepublic int getCount() {return mBlackNumberList.size();}@Overridepublic Object getItem(int position) {return mBlackNumberList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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;}}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_black_number);// 初始化UIinitUI();// 初始化数据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() {@Overridepublic void onClick(View v) {showAddDialog();}});}/*** 初始化数据*/private void initData() {// 获取数据库中的所有电话号码new Thread(){@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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(){@Overridepublic void handleMessage(@NonNull Message msg) {// 4.告知ListView可以去设置数据适配器了mAdapter = new BlackNumberAdapter();// 5.配置适配器lv_blacknumber.setAdapter(mAdapter);}};// 默认的拦截类型private int mMode = 1;private class BlackNumberAdapter extends BaseAdapter {@Overridepublic int getCount() {return mBlackNumberList.size();}@Overridepublic Object getItem(int position) {return mBlackNumberList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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() {@Overridepublic 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;}}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_black_number);// 初始化UIinitUI();// 初始化数据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() {@Overridepublic void onClick(View v) {showAddDialog();}});}/*** 初始化数据*/private void initData() {// 获取数据库中的所有电话号码new Thread(){@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onClick(View v) {// 关闭对话框dialog.dismiss();}});dialog.show();}}
10.拓展功能——ListView的优化
我们在项目中使用了ListView
来以列表的形式展示数据库中的数据,并且和用户进行交互。但若有大量数据(百条数据)插入时,进行查询后并在ListView
控件上浏览容易导致占用内存过大的问题,造成ANR(即主线程7s后无响应)的问题。
终其原因,是因为getView()
的频繁复用,为了优化,需要使用到该方法的第二个参数,即convertView
,原理如下图所示:
改造BlackNumberActivity中的getView()
,进行相应优化(复用convertView
),代码如下:
@Overridepublic View getView(final int position, View convertView, ViewGroup parent) {View view = null;if (convertView == null){// 创建Viewview = View.inflate(getApplicationContext(), R.layout.listview_blacknumber_item, null);}else {// 复用Viewview = 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() {@Overridepublic 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
有点多余,于是可以进一步优化成以下代码:
@Overridepublic View getView(final int position, View convertView, ViewGroup parent) {if (convertView == null){// 创建ViewconvertView = 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() {@Overridepublic 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 {@Overridepublic int getCount() {return mBlackNumberList.size();}@Overridepublic Object getItem(int position) {return mBlackNumberList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(final int position, View convertView, ViewGroup parent) {// 创建ViewHolder内部类ViewHolder holder = null;// (1).复用convertViewif (convertView == null){// 创建ViewconvertView = 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() {@Overridepublic 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(){@Overridepublic 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 {@Overridepublic int getCount() {return mBlackNumberList.size();}@Overridepublic Object getItem(int position) {return mBlackNumberList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(final int position, View convertView, ViewGroup parent) {// 创建ViewHolder内部类ViewHolder holder = null;// (1).复用convertViewif (convertView == null){// 创建ViewconvertView = 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() {@Overridepublic 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;}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_black_number);// 初始化UIinitUI();// 初始化数据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() {@Overridepublic void onClick(View v) {showAddDialog();}});// 监听其滚动状态lv_blacknumber.setOnScrollListener(new AbsListView.OnScrollListener() {@Overridepublic 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(){@Overridepublic 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();}}}}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {// 滚动过程中调用方法}});}/*** 初始化数据*/private void initData() {// 获取数据库中的所有电话号码new Thread(){@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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="/apk/res/android"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".activity.SettingActivity"android:orientation="vertical"><TextViewstyle="@style/TitleStyle"android:text="设置中心"/><!-- 自动更新 --><com.example.mobilesafe.view.SettingItemViewxmlns:mobilesafe="/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.SettingItemViewxmlns:mobilesafe="/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.SettingClickViewandroid:id="@+id/scv_toast_style"android:layout_width="match_parent"android:layout_height="wrap_content" /><!-- 电话归属地显示——位置设置 --><com.example.mobilesafe.view.SettingClickViewandroid:id="@+id/scv_location"android:layout_width="match_parent"android:layout_height="wrap_content" /><!-- 电话归属地显示设置 --><com.example.mobilesafe.view.SettingItemViewxmlns:mobilesafe="/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;// 自定义组合控件SettingClickViewprivate SettingClickView scv_toast_style;@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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;@Overridepublic 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 {@Overridepublic 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();}}}}@Overridepublic IBinder onBind(Intent intent) {throw new UnsupportedOperationException("Not yet implemented");}@Overridepublic 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;@Overridepublic 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 {@Overridepublic 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{// 手动重写,电话状态发生改变时会触发的方法@Overridepublic 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();}}}@Overridepublic IBinder onBind(Intent intent) {throw new UnsupportedOperationException("Not yet implemented");}@Overridepublic void onDestroy() {super.onDestroy();if (mInnerSmsReceiver != null){unregisterReceiver(mInnerSmsReceiver);}}}
由于涉及到挂断电话的操作,需要在清单文件中声明相应权限,代码如下:
<uses-permission android:name="android.permission.CALL_PHONE"/>