文章目录

  • 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="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"><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应该拥有三个字段:

字段名 字段内容 字段类型
_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);}@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="http://schemas.android.com/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="http://schemas.android.com/apk/res/android"><!-- 选中状态:绿色垃圾桶  --><item android:state_pressed="true" android:drawable="@drawable/main_clean_icon_pressed"/><!-- 未选中状态:灰色垃圾桶  --><item android:drawable="@drawable/main_clean_icon"/>
</selector>

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

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

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

package com.example.mobilesafe.activity;import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.BlackNumberDao;
import com.example.mobilesafe.domain.BlackNumberInfo;import java.util.List;public class BlackNumberActivity extends AppCompatActivity {private Button btn_add;private ListView lv_blacknumber;private BlackNumberDao mDao;private List<BlackNumberInfo> mBlackNumberList;private BlackNumberAdapter mAdapter;private Handler mHandler = new Handler(){@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="http://schemas.android.com/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="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"><TextViewstyle="@style/TitleStyle"android:text="设置中心"/><!-- 自动更新 --><com.example.mobilesafe.view.SettingItemViewxmlns: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.SettingItemViewxmlns: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.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="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;// 自定义组合控件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.aidlNeighboringCellInfo.aidl,代码如下:

package com.example.mobilesafe.service;import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.telephony.PhoneStateListener;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;import com.example.mobilesafe.dao.BlackNumberDao;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class BlackNumberService extends Service {private InnerSmsReceiver mInnerSmsReceiver;private BlackNumberDao mDao;private TelephonyManager mSystemService;private MyPhoneStateListener mPhoneStateListener;@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"/>

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

  1. 学习笔记之《Android项目实战——手机安全卫士》

    [Android项目实战-手机安全卫士] 目标:快速积累开发经验,具备中级Android工程师能力. 如遇到难以理解的逻辑或功能,可以先将程序打断点观察程序的执行逻辑. 第一章项目简介:欢迎界面.主界 ...

  2. android在使用单位方面,《Android项目实战——手机安全卫士》_面试题答案.docx

    <Android项目实战--手机安全卫士>_面试题答案 <Android项目实战--手机安全卫士>面试题答案第1章项目简介请问Android程序的真正入口是什么.Android ...

  3. Android 程序员不得不收藏的 90+ 个人博客(持续更新,android项目实战手机安全卫士

    来自滴滴出行,Android 开发助手 开发者,android-open-project 维护者 ,android-open-project-analysis 维护者. 中二病也要开发 ANDROID ...

  4. Android 简单的视频录制,android项目实战手机安全卫士

    */ public static Camera getDefaultCamera(int <Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义> [docs. ...

  5. 基于android开发的手机安全卫士

    随着智能手机和网络的完美结合,使得智能机的功能越来越强大,浏览网页.网络购物.视频对话都普及到各个手机终端,然而手机平台越广泛,存在的危险就越大,越来越多的安全问题出现在手机的日常运用中.当我们以为只 ...

  6. Android项目实战手机安全卫士(02)

    目录 项目结构图 源代码 运行结果 项目源代码 项目结构图 源代码 清单 01.  SplashActivity.java package com.coderdream.mobilesafe.acti ...

  7. Android毕业设计——基于Android+Eclipse的手机安全卫士设计与实现(毕业论文+程序源码)——手机安全卫士

    基于Android+Eclipse的手机安全卫士设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于Android+Eclipse的手机安全卫士设计与实现,文章末尾附有本毕业设计的论文和源码下 ...

  8. Android项目实战--手机卫士

    Android项目实战--手机卫士--结束 很久都没有来更新博客了,之前一直忙着工作的事,接触到了一些以前从来没有接触过的东西,真的挺有挑战性的,但也有很多的无奈,但也学习到了很多东西,我会慢慢的写到 ...

  9. Android项目:手机安全卫士(12)—— 通讯卫士之电话短信黑名单设置与拦截

    版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] Android项目:手机安全卫士(12)-- 通讯卫士之电话.短信黑名单设置与拦截 1 介绍 今天进入新的功能开发了:通讯卫士, ...

最新文章

  1. 2022-2028年中国交通建设PPP模式深度分析及发展战略研究报告(全卷)
  2. git reset --hard xxxxxxx
  3. 关于 uniqueidentifier
  4. Mongodb监控指标
  5. 桑文锋对话菲利普·科特勒:数字化是营销的未来
  6. Jmeter_前端RSA加密下的登陆模拟_引用js文件实现(转)
  7. Shell——流程控制(if、case、for、while)
  8. STM32----摸石头过河系列(六)
  9. KVM(五)libvirt 介绍
  10. ❤️作为测试行业的过来人,宝贵的经验分享给刚入行的你
  11. Roland SRX Series for Mac - 罗兰SRX系列音频插件合集
  12. 图像效果的一些专业测试工具和指标:
  13. Transforms的结构和用法
  14. 如何让客户接受你的价格比别人更高?
  15. 多传感器数据融合简介(转)
  16. Python+Selenium多线程基础微博爬虫
  17. CAP原理与传统的ACID
  18. 数据科学与大数据分析项目练习-3将Apriori算法应用于R中提供的“Groceries”数据集
  19. 点晴信息技术告诉您点晴OA是如何做出色的免费OA系统
  20. c罩杯尺码_教你三步正确测量内衣尺码!

热门文章

  1. 2011年计算机三级考试PC技术知识要点(32)
  2. twitch.tv打不开_开发人员真人秀-程序员的Twitch.tv在哪里?
  3. 断网演练中遇到的问题及总结
  4. Python实现坦克大战
  5. 谷歌“败走麦城”,宣布解散健康部门Google Health
  6. linux安装rvm,在CentOS上安装rvm
  7. 【测试分析案例】Parasoft案例研究:医疗器械软件验证与合规性
  8. EIP-191:签名数据标准
  9. 酷炫背景粒子插件particles.js星空背景使用示例源码 - 附演示及下载地址
  10. Tomb.Finance TVL突破1B大关