本帖最后由 小鲁哥哥 于 2017-4-12 14:25 编辑
【济南中心】Android课程同步笔记day06:Android应用之安全卫士
下拉加载更多的实现
每次滑动到底部去加载下一个20条数据,需要去监听listview滑动的状态了,找到initEvent方法给listview注册滑动的监听,实现OnScrollListener 接口,然后实现里面的两个方法,根据当前可见的最后一个条目的位置lastVisiblePosition,来判断当前数据是否是最后一条,从而决定是否要再次分页加载数据,代码如下:
[Java] 纯文本查看 复制代码 @Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// 当滑动的状态发生改变时
// Log.d(TAG, "onScrollStateChanged");
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// 当滑动时发生
// Log.d(TAG, "onScroll");
// firstVisibleItem:第一个可见的条目的position
// visibleItemCount:可见条目的数量
// totalItemCount:总共的条目数量
if (mAdapter == null || mDatas == null) {
return;
}
// 获得最后一个可见的条目
int lastVisiblePosition = mListView.getLastVisiblePosition();
// 必须滑动到底部
if (lastVisiblePosition == mAdapter.getCount() - 1) {
// 滑动到底部
// 当前正在加载更多,就不往下执行
if (isLoadingMore) {
return;
}
// 是否已经是最多了
if (isLoadAll) {
return;
}
// 开始加载更多了
Log.d(TAG, "开始加载更多了");
isLoadingMore = true;
mLloading.setVisibility(View.VISIBLE);
new Thread(new Runnable() {
@Override
public void run() {
// 加载更多的数据
final List<BlackBean> part = mDao.findPart(mPageSize,
mAdapter.getCount());
if (part == null || part.size() < mPageSize) {
isLoadAll = true;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mLloading.setVisibility(View.GONE);
// 添加部分
mDatas.addAll(part);
// UI更新
mAdapter.notifyDataSetChanged();
// 加载更多结束
isLoadingMore = false;
}
});
}
}).start();
}
}
设置中心增加骚扰拦截开关
显示效果如下: 通过控制开关的打开与关闭来控制骚扰拦截服务的开启与关闭需要去判断如果当前骚扰拦截服务是开启的状态则关闭,否则打开;那么这时我们就需要先提前去将骚扰拦截的服务创建出来:[Java] 纯文本查看 复制代码 public class CallSmsSafeService extends Service {
private static final String TAG = "CallSmsSafeService";
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "开启骚扰拦截的服务");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "关闭骚扰拦截的服务");
}
}
因为需要判断服务是否正在运行,所以这里我们抽取出来了一个工具类专门用于判断服务是否正在运行:
[Java] 纯文本查看 复制代码 public class ServiceStateUtils {
/**
* 判断服务是否运行
*
* @param context
* @param clazz
* @return
*/
public static boolean isServiceRunning(Context context,
Class<? extends Service> clazz) {
ActivityManager am = (ActivityManager) context
.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningServiceInfo> services = am
.getRunningServices(Integer.MAX_VALUE);
for (RunningServiceInfo info : services) {
String serviceName = info.service.getClassName();
if (clazz.getName().equals(serviceName)) {
// 服务在运行中
return true;
}
}
return false;
}
}
现在服务也有了,判断服务是否正在运行的工具类也有了那么当点击骚扰拦截开关按钮的方法即可这样实现:
[Java] 纯文本查看 复制代码 /**
* 骚扰拦截的开关设置
*/
private void clickCallSmsSafe() {
// 如果服务开启,就关闭,否则相反
if (ServiceStateUtils.isServiceRunning(this, CallSmsSafeService.class)) {
// 就关闭
Intent service = new Intent(this, CallSmsSafeService.class);
stopService(service);
// ui
mSivCallSmsSafe.setToggleState(false);
} else {
// 开启
Intent service = new Intent(this, CallSmsSafeService.class);
startService(service);
// ui
mSivCallSmsSafe.setToggleState(true);
}
}
短信拦截
在服务中我们即可去动态注册一个广播接收者来监听短信的到来,如果该号码是我们黑名单中的号码就中断该广播,具体代码如下所示:[Java] 纯文本查看 复制代码 public class CallSmsSafeService extends Service {
private static final String TAG = "CallSmsSafeService";
private BlackDao mDao;
private SmsReceiver mSmsReceiver;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "开启骚扰拦截的服务");
mDao = new BlackDao(this);
// 1. 短信的拦截
mSmsReceiver = new SmsReceiver();
IntentFilter filter = new IntentFilter();
// <intent-filter android:priority="1000" >
// <!-- 接收短信的 -->
// <action android:name="android.provider.Telephony.SMS_RECEIVED" />
// </intent-filter>
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
filter.addAction("android.provider.Telephony.SMS_RECEIVED");
registerReceiver(mSmsReceiver, filter);
}
private class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 获得短信,发送者的号码,判断号码是否是需要拦截
Object[] objs = (Object[]) intent.getExtras().get("pdus");
for (Object obj : objs) {
SmsMessage sms = SmsMessage.createFromPdu((byte[]) obj);
// 发送者
String sender = sms.getOriginatingAddress();
int type = mDao.findType(sender);
if (type == BlackBean.TYPE_SMS || type == BlackBean.TYPE_ALL) {
// 需要拦截短信,不让用户接收短信
Log.d(TAG, "拦截" + sender + "发送的短信");
abortBroadcast();
}
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "关闭骚扰拦截的服务");
unregisterReceiver(mSmsReceiver);
}
}
电话拦截要实现黑名单中电话拦截的功能,为了侦听电话状态,我们需要获得系统的电话管理器等对象,具体代码如下所示:
[AppleScript] 纯文本查看 复制代码 public void onCreate() {
super.onCreate();
Log.d(TAG, "开启骚扰拦截的服务");
mDao = new BlackDao(this);
// 1. 短信的拦截
mSmsReceiver = new SmsReceiver();
IntentFilter filter = new IntentFilter();
// <intent-filter android:priority="1000" >
// <!-- 接收短信的 -->
// <action android:name="android.provider.Telephony.SMS_RECEIVED" />
// </intent-filter>
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
filter.addAction("android.provider.Telephony.SMS_RECEIVED");
registerReceiver(mSmsReceiver, filter);
// 2. 电话拦截
// 1) 知道电话什么时候拨入
// 2) 通过代码去挂电话
mTm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
mCallListener = new CallListener();
// 注册电话的监听
mTm.listen(mCallListener, PhoneStateListener.LISTEN_CALL_STATE);
}
注意oncreate中注册了电话监听器一定要记得先在ondestroy中注销: [Java] 纯文本查看 复制代码 @Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "关闭骚扰拦截的服务");
// 注销短信接受者
unregisterReceiver(mSmsReceiver);
// 注销电话的监听
mTm.listen(mCallListener, PhoneStateListener.LISTEN_NONE);
}
其中,CallListener 是我们自定义的一个侦听电话状态的监听器,首先我们来看一下这个电话侦听器是如何监听的,书写以下的代码: [Java] 纯文本查看 复制代码 private class CallListener extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, final String incomingNumber) {
// * @see TelephonyManager#CALL_STATE_IDLE:闲置状态
// * @see TelephonyManager#CALL_STATE_RINGING:响铃状态
// * @see TelephonyManager#CALL_STATE_OFFHOOK:摘机状态,接听电话状态
// state
// incomingNumber:拨入的号码
if (state == TelephonyManager.CALL_STATE_RINGING) {
// 响铃时,正在拨入时
int type = mDao.findType(incomingNumber);
// 判断是否是被拦截的电话
if (type == BlackBean.TYPE_CALL || type == BlackBean.TYPE_ALL) {
// 通过代码去挂断电话
Log.d(TAG, "通过代码去挂断" + incomingNumber + "电话");
endCall();
}
}
}
}
当电话铃响时需要挂断电话并且不让该记录显示在界面上,而Google 工程师为了手机的安全性隐藏了挂断电话的服务方法,因此要实现挂断电话的操作只能通过反射获取底层服务。这里,我们使用反射的机制获取系统底层挂断电话的方法,如下所示: [Java] 纯文本查看 复制代码 /**
* 挂断电话
*/
private void endCall() {
// 1) ITelephony 实例
// a. ITelephony.aidl添加到代码中--》ITelephony.java
ITelephony iTelephony = null;
// ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
// IBinder ibinder = ServiceManager
// .getService(Context.TELEPHONY_SERVICE);
try {
// 获取到ServiceManager 的类名
Class<?> clazz = Class
.forName("android.os.ServiceManager");
// 暴力反射(第一个参数:表示方法名字,第二个参数:表示方法的类型)
Method method = clazz.getMethod("getService",
String.class);
// 调用当前的方法(第一个参数:表示谁调用当前方法)
IBinder ibinder = (IBinder) method.invoke(null,
Context.TELEPHONY_SERVICE);
iTelephony = ITelephony.Stub.asInterface(ibinder);
// 2) ITelephony.endCall();挂断电话
iTelephony.endCall();// 异步操作
} catch (Exception e) {
e.printStackTrace();
}
}
endCall()方法用于挂断黑名单的呼入电话,该段代码中首先通过反射获取到ServiceManager 字节码,然后通过该字节码获取getService()方法,该方法接收一个String 类型的参数,然后通过invoke()执行getService()方法,由于getService()方法是静态的,因此invoke()的第一个参数可以为null,第二个参数是TELEPHONY_SERVICE。由于getService()方法的返回值是一个IBinder 对象(远程服务的代理类),因此需要使用AIDL 的规则将其转化为接口类型,由于我们的操作是挂断电话,因此需要使用与电话相关的ITelephony.aidl,然后调用接口中的endCall()方法将电话挂断即可。
需要注意的是,与电话相关的操作一般都使用TelephonyManager 类,但是由于挂断电话的方法在ITelephony接口中,而这个接口是隐藏的(@hide)在开发时看不到,因此需要使用ITelephony.aidl。在使用ITelephony.aidl时,需要创建一个与它包名一致的包com.android.internal.telephony然后把系统的ITelephony.aidl 文件拷贝进来,同时ITelephony.aidl 接口关联了NeighboringCellInfo.aidl,所以也需要一并拷贝进来。不过要注意的是,NeighboringCellInfo.aidl 所在的的包名是android.telephony,因此需要新建一个android.telephony包,然后把NeighboringCellInfo.aidl 放到包该包中。如图所示: 另外,想要挂断电话需要配置一个权限,如下所示。 <uses-permissionandroid:name="android.permission.CALL_PHONE" />
删除呼叫记录
上面我们已经实现了拦截电话的功能,但是打开手机的通话记录会发现通话记录仍然还存在,这样的体验会让用户觉得很奇怪,明明没有来电信息,为什么会出现通话记录呢,这是因为我们没有删除手机系统中通话记录数据库的数据,所以才会显示出来。 想要删除手机系统中的通话记录,我们需要注册一个内容解析者来操作手机的通话记录数据库,在来电响铃时的逻辑代码如下所示:
[Java] 纯文本查看 复制代码 /**
* 挂断电话
*
* @param incomingNumber
* :来电号码
*/
private void endCall(final String incomingNumber) {
// 1) ITelephony 实例
// a. ITelephony.aidl添加到代码中--》ITelephony.java
ITelephony iTelephony = null;
// ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
// IBinder ibinder = ServiceManager
// .getService(Context.TELEPHONY_SERVICE);
try {
// 获取到ServiceManager 的类名
Class<?> clazz = Class.forName("android.os.ServiceManager");
// 暴力反射(第一个参数:表示方法名字,第二个参数:表示方法的类型)
Method method = clazz.getMethod("getService", String.class);
// 调用当前的方法(第一个参数:表示谁调用当前方法)
IBinder ibinder = (IBinder) method.invoke(null,
Context.TELEPHONY_SERVICE);
iTelephony = ITelephony.Stub.asInterface(ibinder);
// 2) ITelephony.endCall();挂断电话
iTelephony.endCall();// 异步操作
// 通过线程睡一会(不推荐)
// Thread.sleep(300);
// 删除通话记录
final ContentResolver cr = getContentResolver();
final Uri url = CallLog.Calls.CONTENT_URI;
// content://calls
// content://calls/1/100
// notifyForDescendents: true
// -->url下面的任何分支发生改变,都会通知对应的observer
// fasle-->只通知当前的url
cr.registerContentObserver(url, true, new ContentObserver(
new Handler()) {
@Override
public void onChange(boolean selfChange) {
String where = CallLog.Calls.NUMBER + "=?";
String[] selectionArgs = new String[] { incomingNumber };
cr.delete(url, where, selectionArgs);
// 注销
cr.unregisterContentObserver(this);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
注意添加权限: android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
常用工具-号码归属地
常用工具中有这样的一个查询归属地的功能,它是用于查询手机号码的归属地,功能的展示界面如图:
号码归属地界面代码实现:
[Java] 纯文本查看 复制代码 public class NumberAddressActivity extends Activity implements OnClickListener {
private EditText mEtNumber;
private Button mBtnQuery;
private TextView mTvAddress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_number_address);
initView();
initEvent();
}
private void initView() {
mEtNumber = (EditText) findViewById(R.id.na_et_number);
mBtnQuery = (Button) findViewById(R.id.na_btn_query);
mTvAddress = (TextView) findViewById(R.id.na_tv_address);
}
private void initEvent() {
mBtnQuery.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v == mBtnQuery) {
clickQuery();
}
}
private void clickQuery() {
// 如果输入框没有内容,就抖动
String number = mEtNumber.getText().toString().trim();
if (TextUtils.isEmpty(number)) {
// 抖动
// Animation shake = AnimationUtils.loadAnimation(this,
// R.anim.shake);
// mEtNumber.startAnimation(shake);
// android 所有的xml类型的文件其实会映射成java代码
TranslateAnimation animation = new TranslateAnimation(0, 10, 0, 0);
animation.setDuration(1000);
animation.setInterpolator(new CycleInterpolator(10));
mEtNumber.startAnimation(animation);
// 不能为空的提示
Toast.makeText(this, "号码不能为空", Toast.LENGTH_SHORT).show();
return;
}
}
}
其中,在点击查询号码的时候,如果号码编辑框为空时会有一个抖动的动画效果,第一种方式可以通过xml的方式实现,该动画shake.xml放在anim 文件下,如下所示: [XML] 纯文本查看 复制代码 <translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromXDelta="0"
android:interpolator="@anim/cycle_7"
android:toXDelta="10" /> shake.xml 中的interpolator 是一个函数,它计算动画如何播放 [XML] 纯文本查看 复制代码 <cycleInterpolator xmlns:android="http://schemas.android.com/apk/res/android" android:cycles="7" /> 拷贝数据库 号码所查询的是一个数据中的数据,数据库的内容如下图所示,数据库address.db 文件我们是放在assets 目录下的应用程序在欢迎界面就已将数据库文件拷贝到应用自身的数据库文件夹中,然后进行之后的一系列查询操作,这里在欢迎界面中用于拷贝数据库的代码如下所示: [Java] 纯文本查看 复制代码 /**
* 拷贝归属地数据库
*/
private void copyNumberAddressDB() {
File file = new File(getFilesDir(), "address.db");
if (file.exists()) {
return;
}
AssetManager assets = getAssets();
try {
InputStream in = assets.open("address.zip");
FileOutputStream fos = new FileOutputStream(file);
GZipUtils.unzip(in, fos);
} catch (IOException e) {
e.printStackTrace();
}
} 这里用了一个GzipUtils的工具进行zip的解压,这是因为由于源文件db有16M多这样子运行程序会特别慢,采用zip压缩,拷贝的时候解压速度会比较快. [Java] 纯文本查看 复制代码 public class GZipUtils {
/**
* 压缩
*
* @param srcPath
* @param destPath
* @throws IOException
*/
public static void zip(String srcPath, String destPath) throws IOException {
zip(new File(srcPath), new File(destPath));
}
/**
* 压缩
*
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void zip(File srcFile, File destFile) throws IOException {
zip(new FileInputStream(srcFile), new FileOutputStream(destFile));
}
/**
* 压缩
*
* @param in
* @param out
* @throws IOException
*/
public static void zip(InputStream in, OutputStream out) throws IOException {
// 流的操作
// 标准的输入流---> (Gzip输出流 --> 标准的输出流)
// 包装
byte[] buffer = new byte[1024];
int len = -1;
GZIPOutputStream gos = new GZIPOutputStream(out);
try {
while ((len = in.read(buffer)) != -1) {
gos.write(buffer, 0, len);
}
} finally {
closeIO(in);
closeIO(gos);
}
}
/**
* 解压
*
* @param srcPath
* @param destPath
* @throws IOException
*/
public static void unzip(String srcPath, String destPath)
throws IOException {
unzip(new File(srcPath), new File(destPath));
}
/**
* 解压
*
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void unzip(File srcFile, File destFile) throws IOException {
unzip(new FileInputStream(srcFile), new FileOutputStream(destFile));
}
/**
* gzip的解压
*
* @param in
* @param out
* @throws IOException
*/
public static void unzip(InputStream in, OutputStream out)
throws IOException {
// 流的操作
// (标准的输入流--GZip的输入流)-->标准流的方式输出
GZIPInputStream gis = new GZIPInputStream(in);
byte[] buffer = new byte[1024];
int len = -1;
try {
while ((len = gis.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} finally {
closeIO(gis);
closeIO(out);
}
}
public static void closeIO(Closeable io) {
if (io != null) {
try {
io.close();
} catch (IOException e) {
e.printStackTrace();
}
io = null;
}
}
} 号码查询实现 据库拷贝好了后,下面就是进行具体号码查询的逻辑,这里所要查询的是数据库中的信息,创建一个NumberAddressDao进行数据的查询操作: [Java] 纯文本查看 复制代码 public class NumberAddressDao {
public static String findAddress(Context context, String number) {
File file = new File(context.getFilesDir(), "address.db");
String path = file.getAbsolutePath();
SQLiteDatabase db = SQLiteDatabase.openDatabase(path, null,
SQLiteDatabase.OPEN_READONLY);
String address = "未知号码";
// 13,14,15,17,18 ---> 11位
// 1[34578]\d
String reg = "^1[34578]\\d{9}$";
boolean isPhone = number.matches(reg);
if (isPhone) {
String prefix = number.substring(0, 7);
String sql = "select cardtype from info where mobileprefix=?";
Cursor cursor = db.rawQuery(sql, new String[] { prefix });
if (cursor != null) {
if (cursor.moveToNext()) {
address = cursor.getString(0);
}
cursor.close();
}
} else {
switch (number.length()) {
case 3:
// 110,119,120
address = "警报电话";
break;
case 4:
// 5556
address = "模拟器&亲情号码";
break;
case 5:
// 95555
address = "银行号码";
break;
case 7:
case 8:
// 8123456
// 81234567
address = "本地号码";
break;
case 10:
case 11:
case 12:
// 010-8123456外地
// 010-81234567外地
// 0755-8123456外地
// 0755-81234567外地
// 查询3位
String prefix = number.substring(0, 3);
String sql = "select city from info where area=?";
Cursor cursor = db.rawQuery(sql, new String[] { prefix });
if (cursor != null) {
if (cursor.moveToNext()) {
address = cursor.getString(0);
}
cursor.close();
}
if ("未知号码".equals(address)) {
// 3位没有查出,查询4位
prefix = number.substring(0, 4);
sql = "select city from info where area=?";
cursor = db.rawQuery(sql, new String[] { prefix });
if (cursor != null) {
if (cursor.moveToNext()) {
address = cursor.getString(0);
}
cursor.close();
}
}
break;
default:
break;
}
}
db.close();
return address;
}
}
|