之前我们完善了“高级工具”模块中第四个功能——程序锁的布局和功能实现,这一节中我们将完善更多细节,包括条目在操作时的初始化动画事件监听和点击锁集合后数据库修改等操作。
该功能需要解决以下问题:
修改AppLockActivity,完善相应逻辑,代码如下:
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.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.AppInfoDao;
import com.example.mobilesafe.dao.AppLockDao;
import com.example.mobilesafe.domain.AppInfo;
import java.util.ArrayList;
import java.util.List;
public class AppLockActivity extends AppCompatActivity {
private static final String TAG = "AppLockActivity";
private Button btn_unlock;
private Button btn_lock;
private LinearLayout ll_unlock;
private LinearLayout ll_lock;
private TextView tv_unlock;
private TextView tv_lock;
private ListView lv_unlock;
private ListView lv_lock;
private List<AppInfo> mAppInfoList;
private List<AppInfo> mLockList;
private List<AppInfo> mUnLockList;
private AppLockDao mDao;
private MyAdapter mLockAdapter;
private MyAdapter mUnLockAdapter;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
// 6.接收到消息,填充已加锁和未加锁的数据适配器中
mLockAdapter = new MyAdapter(true);
lv_lock.setAdapter(mLockAdapter);
mUnLockAdapter = new MyAdapter(false);
lv_unlock.setAdapter(mUnLockAdapter);
}
};
private TranslateAnimation mTranslateAnimation;
class MyAdapter extends BaseAdapter{
private boolean isLock;
/**
* 用于区分已加锁和未加锁应用的标识
* @param isLock 用于区分已加锁和未加锁应用的标示,true是已加锁,false是未加锁
*/
public MyAdapter(boolean isLock) {
this.isLock = isLock;
}
@Override
public int getCount() {
if (isLock){
tv_lock.setText("未加锁应用:" + mLockList.size());
return mLockList.size();
}else {
tv_unlock.setText("已加锁应用:" + mUnLockList.size());
return mUnLockList.size();
}
}
@Override
public AppInfo getItem(int position) {
if (isLock){
return mLockList.get(position);
}else {
return mUnLockList.get(position);
}
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null){
convertView = View.inflate(getApplicationContext(), R.layout.listview_islock_item, null);
holder = new ViewHolder();
holder.iv_icon = convertView.findViewById(R.id.iv_icon);
holder.tv_name = convertView.findViewById(R.id.tv_name);
holder.iv_lock = convertView.findViewById(R.id.iv_lock);
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}
final AppInfo appInfo = getItem(position);
final View animationView = convertView; // 执行动画的View
holder.iv_icon.setBackgroundDrawable(appInfo.getIcon());
holder.tv_name.setText(appInfo.getName());
if(isLock){
holder.iv_lock.setBackgroundResource(R.drawable.lock);
}else {
holder.iv_lock.setBackgroundResource(R.drawable.unlock);
}
holder.iv_lock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 0.添加加锁/解锁的动画效果(有bug,动画效果比数据操作慢)
// animationView.startAnimation(mTranslateAnimation);
// 0.对动画的执行过程做事件监听,监听到动画执行完成后,再去移除集合中的数据,再刷新界面(改进)
animationView.startAnimation(mTranslateAnimation);
mTranslateAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// 0.动画执行结束后调用的方法
if (isLock){
// 已加锁切换成未加锁
// 1.已加锁集合减少一个,未加锁集合添加一个,对象就是getItem方法获取的对象
mLockList.remove(appInfo);
mUnLockList.add(appInfo);
// 2.从已加锁的数据库中删除一条数据
mDao.delete(appInfo.getPackagename());
// 3.刷新数据适配器
mLockAdapter.notifyDataSetChanged();
mUnLockAdapter.notifyDataSetChanged();
}else {
// 未加锁切换成已加锁
// 1.已加锁集合增加一个,未加锁集合减少一个,对象就是getItem方法获取的对象
mLockList.add(appInfo);
mUnLockList.remove(appInfo);
// 2.从已加锁的数据库中删除一条数据
mDao.insert(appInfo.getPackagename());
// 3.刷新数据适配器
mLockAdapter.notifyDataSetChanged();
mUnLockAdapter.notifyDataSetChanged();
}
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
});
return convertView;
}
}
static class ViewHolder{
ImageView iv_icon;
TextView tv_name;
ImageView iv_lock;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_lock);
// 初始化UI
initUI();
// 初始化数据
initData();
// 初始化动画事件监听
initAnimation();
}
/**
* 初始化UI
*/
private void initUI() {
btn_unlock = findViewById(R.id.btn_unlock);
btn_lock = findViewById(R.id.btn_lock);
ll_unlock = findViewById(R.id.ll_unlock);
ll_lock = findViewById(R.id.ll_lock);
tv_unlock = findViewById(R.id.tv_unlock);
tv_lock = findViewById(R.id.tv_lock);
lv_unlock = findViewById(R.id.lv_unlock);
lv_lock = findViewById(R.id.lv_lock);
btn_unlock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.已加锁列表隐藏,未加锁列表显示
ll_lock.setVisibility(View.GONE);
ll_unlock.setVisibility(View.VISIBLE);
// 2.未加锁变成浅色图片,已加锁变成深色图片
btn_unlock.setBackgroundResource(R.drawable.tab_left_pressed);
btn_lock.setBackgroundResource(R.drawable.tab_right_default);
}
});
btn_lock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 1.已加锁列表显示,未加锁列表隐藏
ll_lock.setVisibility(View.VISIBLE);
ll_unlock.setVisibility(View.GONE);
// 2.未加锁变成浅色图片,已加锁变成深色图片
btn_unlock.setBackgroundResource(R.drawable.tab_left_default);
btn_lock.setBackgroundResource(R.drawable.tab_right_pressed);
}
});
}
/**
* 初始化数据,区分已加锁和未加锁应用的集合
*/
private void initData() {
new Thread(){
@Override
public void run() {
// 1.获取所有手机中的应用
mAppInfoList = AppInfoDao.getAppInfoList(getApplicationContext());
// 2.区分已加锁应用和未加锁应用
mLockList = new ArrayList<>();
mUnLockList = new ArrayList<>();
// 3.获取数据库中已加锁应用包名的集合
mDao = AppLockDao.getInstance(getApplicationContext());
List<String> lockPackageList = mDao.queryAll();
for (AppInfo appInfo : mAppInfoList) {
// 4.如果循环到的应用包名在数据库中,则说明是已加锁应用
if (lockPackageList.contains(appInfo.getPackagename())){
mLockList.add(appInfo);
}else {
mUnLockList.add(appInfo);
}
}
// 5.告知主线程,可以使用维护的数据
mHandler.sendEmptyMessage(0);
}
}.start();
}
/**
* 初始化动画事件监听(平移自身的一个宽度大小)
*/
private void initAnimation() {
mTranslateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,1,
Animation.RELATIVE_TO_SELF,0,Animation.RELATIVE_TO_SELF,0);
mTranslateAnimation.setDuration(500);
}
}
锁定的应用在被打开时需要输入指定密码,如图中红框所示:
为了让程序锁在应用退出时依旧生效,需要将该功能维护到Service
组件中,应按照以下步骤实现:
首先修改activity_setting.xml,添加自定义条目,作为是否开启程序锁设置项的布局,代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.SettingActivity"
android:orientation="vertical">
<TextView
style="@style/TitleStyle"
android:text="设置中心"/>
<com.example.mobilesafe.view.SettingItemView
xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
android:id="@+id/siv_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
mobilesafe:destitle="自动更新设置"
mobilesafe:desoff="自动更新已关闭"
mobilesafe:deson="自动更新已开启"/>
<com.example.mobilesafe.view.SettingItemView
xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
android:id="@+id/siv_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
mobilesafe:destitle="电话归属地的显示设置"
mobilesafe:desoff="归属地的显示已关闭"
mobilesafe:deson="归属地的显示已开启"/>
<com.example.mobilesafe.view.SettingClickView
android:id="@+id/scv_toast_style"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.example.mobilesafe.view.SettingClickView
android:id="@+id/scv_location"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.example.mobilesafe.view.SettingItemView
xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
android:id="@+id/siv_blacknumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
mobilesafe:destitle="黑名单拦截设置"
mobilesafe:desoff="黑名单拦截已关闭"
mobilesafe:deson="黑名单拦截已开启"/>
<com.example.mobilesafe.view.SettingItemView
xmlns:mobilesafe="http://schemas.android.com/apk/res/com.example.mobilesafe"
android:id="@+id/siv_app_lock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
mobilesafe:destitle="程序锁设置"
mobilesafe:desoff="程序锁已关闭"
mobilesafe:deson="程序锁已开启"/>
LinearLayout>
接着,修改SettingActivity,添加initAppLock(),作为初始化程序锁的方法,代码如下:
/**
* 6.初始化程序锁
*/
private void initAppLock() {
final SettingItemView siv_app_lock = findViewById(R.id.siv_app_lock);
boolean isRunning = ServiceUtil.isRunning(this, "com.example.mobilesafe.service.WatchDogService");
siv_app_lock.setCheck(isRunning);
siv_app_lock.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean isCheck = siv_app_lock.isCheck();
siv_app_lock.setCheck(!isCheck);
if (!isCheck){
// 开启服务
startService(new Intent(getApplicationContext(), WatchDogService.class));
}else {
// 关闭服务
stopService(new Intent(getApplicationContext(), WatchDogService.class));
}
}
});
}
在service包下新增WatchDogService,作为负责程序锁的服务,需要去创建一个广播接受者过滤掉已经通过程序锁的应用,否则会一直循环,代码如下:
package com.example.mobilesafe.service;
import android.app.ActivityManager;
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 com.example.mobilesafe.activity.EnterPsdActivity;
import com.example.mobilesafe.dao.AppLockDao;
import java.util.List;
public class WatchDogService extends Service {
private boolean isWatch;
private AppLockDao mDao;
private List<String> mPackageNameList;
private InnerReceiver mInnerReceiver;
private String mSkipPackageName;
@Override
public void onCreate() {
super.onCreate();
// 维护一个看门狗的死循环,让其时刻监测现在开启的应用,是否为程序锁中要去拦截的应用
mDao = AppLockDao.getInstance(this);
isWatch = true;
watch();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.intent.action.SKIP");
mInnerReceiver = new InnerReceiver();
registerReceiver(mInnerReceiver,intentFilter);
}
private class InnerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 获取发送广播过程中传递过来的包名,跳过此包名的检测过程
mSkipPackageName = intent.getStringExtra("packagename");
}
}
/**
* 开启死循环任务
*/
private void watch() {
// 1.开启一个死循环
new Thread(){
@Override
public void run() {
// 6.拿此包名在已加锁的包名集合中做比对,如果包含此包名,则需要弹出拦截界面
mPackageNameList = mDao.queryAll();
while (isWatch){
// 2.监测现在正在开启的应用,任务栈
// 3.获取Activity管理者对象
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
// 4.获取正在开启应用的任务栈的方法
List<ActivityManager.RunningTaskInfo> runningTasks = activityManager.getRunningTasks(1);
ActivityManager.RunningTaskInfo runningTaskInfo = runningTasks.get(0);
// 5.获取栈顶的Activity所在应用的包名
String packageName = runningTaskInfo.topActivity.getPackageName();
// 7.进行包名比对,如果包含此包名,则需要进行拦截
if (mPackageNameList.contains(packageName)) {
// 如果现在检测的程序,已经解锁了,则不需要去弹出拦截界面
if (!packageName.equals(mSkipPackageName)){
// 8.弹出拦截界面
Intent intent = new Intent(getApplicationContext(), EnterPsdActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packagename",packageName);
startActivity(intent);
}
}
// 9.因为死循环过多消耗性能,所以需要睡眠(时间片轮转)
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
在activity包下新建名为EnterPsdActivity的Activity,作为程序锁功能中拦截Activity使用的活动,首先修改其布局文件activity_enter_psd.xml,代码如下:
<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.EnterPsdActivity"
android:orientation="vertical">
<TextView
android:id="@+id/tv_app_name"
android:text="拦截应用的名称"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/iv_app_icon"
android:background="@drawable/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/et_psd"
android:hint="请输入解锁密码"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn_submit"
android:text="提交"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
LinearLayout>
修改EnterPsdActivity,完善锁定应用时的跳转逻辑,由于涉及到任务栈之间的切换,需要在清单文件中设定EnterPsdActivity的启动模式为单例模式(SingleInstance
),代码如下:
package com.example.mobilesafe.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.utils.ToastUtil;
public class EnterPsdActivity extends AppCompatActivity {
private String packagename;
private TextView tv_app_name;
private ImageView iv_app_icon;
private EditText et_psd;
private Button btn_submit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_enter_psd);
// 获取包名
packagename = getIntent().getStringExtra("packagename");
// 初始化UI
initUI();
// 初始化数据
initData();
}
/**
* 初始化UI
*/
private void initUI() {
tv_app_name = findViewById(R.id.tv_app_name);
iv_app_icon = findViewById(R.id.iv_app_icon);
et_psd = findViewById(R.id.et_psd);
btn_submit = findViewById(R.id.btn_submit);
}
/**
* 通过传递过来的包名获取拦截应用的图标以及名称
*/
private void initData() {
PackageManager packageManager = getPackageManager();
try {
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packagename,0);
Drawable icon = applicationInfo.loadIcon(packageManager);
String label = applicationInfo.loadLabel(packageManager).toString();
iv_app_icon.setBackgroundDrawable(icon);
tv_app_name.setText(label);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
btn_submit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String psd = et_psd.getText().toString();
if (!TextUtils.isEmpty(psd)){
if (psd.equals("123")){
// 解锁,进入应用,告知看门狗服务不需要再去监听已经解锁的应用,需要发送广播
Intent intent = new Intent("android.intent.action.SKIP");
intent.putExtra("packagename",packagename);
sendBroadcast(intent);
finish();
}else {
ToastUtil.show(getApplicationContext(),"密码错误!");
}
}else {
ToastUtil.show(getApplicationContext(),"请输入密码!");
}
}
});
}
}
由于涉及到获取任务栈的操作,需要在清单文件中声明对应权限,代码如下:
<uses-permission android:name="android.permission.GET_TASKS"/>
<!-- 设定EnterPsdActivity的启动模式 -->
<activity android:name=".activity.EnterPsdActivity" android:launchMode="singleInstance"/>
修改AppLockDao,在插入数据insert()和删除数据delete操作中添加内容解析器,以此让看门狗服务进行判断是否需要重新获取数据,代码如下:
/**
* 4.插入数据
* @param packagename 待插入的应用包名
*/
public void insert(String packagename){
SQLiteDatabase db = mApplockOpenHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("packagename",packagename);
db.insert("applock",null,values);
db.close();
context.getContentResolver().notifyChange(Uri.parse("content://applock/change"),null);
}
/**
* 5.删除数据
* @param packagename 待插入的应用包名
*/
public void delete(String packagename){
SQLiteDatabase db = mApplockOpenHelper.getWritableDatabase();
db.delete("applock","packagename = ?",new String[]{packagename});
db.close();
context.getContentResolver().notifyChange(Uri.parse("content://applock/change"),null);
}
修改WatchDogService,通过内容解析者去获取信息,判断存储应用信息的数据库是否发生变动,代码如下:
package com.example.mobilesafe.service;
import android.app.ActivityManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import com.example.mobilesafe.activity.EnterPsdActivity;
import com.example.mobilesafe.dao.AppLockDao;
import java.util.List;
public class WatchDogService extends Service {
private boolean isWatch;
private AppLockDao mDao;
private List<String> mPackageNameList;
private InnerReceiver mInnerReceiver;
private String mSkipPackageName;
private MyContentObserver mContentObserver;
@Override
public void onCreate() {
super.onCreate();
// 维护一个看门狗的死循环,让其时刻监测现在开启的应用,是否为程序锁中要去拦截的应用
mDao = AppLockDao.getInstance(this);
isWatch = true;
watch();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.intent.action.SKIP");
mInnerReceiver = new InnerReceiver();
registerReceiver(mInnerReceiver,intentFilter);
// 注册监听数据库变化的内容监听器
mContentObserver = new MyContentObserver(new Handler());
getContentResolver().registerContentObserver(Uri.parse("content://applock/change"),true,mContentObserver);
}
class MyContentObserver extends ContentObserver{
public MyContentObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
// 一旦数据库发生改变(插入、删除数据)时调用的方法,需要重新获取包名所在集合的数据
super.onChange(selfChange);
new Thread(){
@Override
public void run() {
mPackageNameList = mDao.queryAll();
}
}.start();
}
}
private class InnerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 获取发送广播过程中传递过来的包名,跳过此包名的检测过程
mSkipPackageName = intent.getStringExtra("packagename");
}
}
/**
* 开启死循环任务
*/
private void watch() {
// 1.开启一个死循环
new Thread(){
@Override
public void run() {
// 6.拿此包名在已加锁的包名集合中做比对,如果包含此包名,则需要弹出拦截界面
mPackageNameList = mDao.queryAll();
while (isWatch){
// 2.监测现在正在开启的应用,任务栈
// 3.获取Activity管理者对象
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
// 4.获取正在开启应用的任务栈的方法
List<ActivityManager.RunningTaskInfo> runningTasks = activityManager.getRunningTasks(1);
ActivityManager.RunningTaskInfo runningTaskInfo = runningTasks.get(0);
// 5.获取栈顶的Activity所在应用的包名
String packageName = runningTaskInfo.topActivity.getPackageName();
// 7.进行包名比对,如果包含此包名,则需要进行拦截
if (mPackageNameList.contains(packageName)) {
// 如果现在检测的程序,已经解锁了,则不需要去弹出拦截界面
if (!packageName.equals(mSkipPackageName)){
// 8.弹出拦截界面
Intent intent = new Intent(getApplicationContext(), EnterPsdActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("packagename",packageName);
startActivity(intent);
}
}
// 9.因为死循环过多消耗性能,所以需要睡眠(时间片轮转)
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public void onDestroy() {
super.onDestroy();
// 将标识符置为false
isWatch = false;
// 注销广播接收者
if (mInnerReceiver != null){
unregisterReceiver(mInnerReceiver);
}
// 注销内容观察者
if (mContentObserver != null){
getContentResolver().unregisterContentObserver(mContentObserver);
}
}
}
为了在弹出输入程序锁密码界面时,按下HOME键后不会显示手机卫士的Activity,需要在清单文件中对EnterPsdActivity的特殊属性进行声明,让其不去显示手机卫士的图标,代码如下:
<activity android:name=".activity.EnterPsdActivity"
android:launchMode="singleInstance"
android:excludeFromRecents="true"/>
为了在弹出输入程序锁密码界面时,按下Back键后不会退出该界面而直接进入该应用,则需要重写按下Back键的点击事件,修改EnterPsdActivity,重写onBackPressed()
方法,代码如下:
@Override
public void onBackPressed() {
super.onBackPressed();
// 跳转到桌面
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}
前面我们完成了“高级工具”模块的实现,接下来需要去实现“手机杀毒”的模块功能,进入该模块后,会出现扫描应用的界面,如图所示:
当应用扫描完毕后,会弹出疑似病毒应用的卸载窗口,如图所示:
扫描列表可以使用LinearLayout
布局来实现,由于每次扫描都会添加扫描的应用信息,即添加一项View
,为了不让View
在显示时相互遮挡,在每一次添加时都给它设定为最开始的坐标;另外,由于应用比较多,考虑到扫描应用时可能显示不全,还需要使用ScrollView
来实现竖直方向的拖拽,如图所示:
修改HomeActivity,修改initData(),添加跳转到“手机杀毒”模块的界面逻辑,代码如下:
/**
* 2.初始化数据
*/
private void initData() {
// 1.初始化每个图标的标题
mTitleStrs = new String[]{"手机防盗","通信卫士","软件管理","进程管理","流量统计","手机杀毒","缓存清理","高级工具","设置中心"};
// 2.初始化每个图标的图像
mDrawableIds = new int[]{R.drawable.home_safe,R.drawable.home_callmsgsafe,R.drawable.home_apps,R.drawable.home_taskmanager,R.drawable.home_netmanager,R.drawable.home_trojan,R.drawable.home_sysoptimize,R.drawable.home_tools,R.drawable.home_settings};
// 3.为GridView设置数据适配器
gv_home.setAdapter(new MyAdapter());
// 4.注册GridView中单个条目的点击事件
gv_home.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position){
case 0:
// 手机防盗
showDialog();
break;
case 1:
// 通信卫士
startActivity(new Intent(getApplicationContext(),BlackNumberActivity.class));
break;
case 2:
// 软件管理
startActivity(new Intent(getApplicationContext(),AppManagerActivity.class));
break;
case 3:
// 进程管理
startActivity(new Intent(getApplicationContext(),ProcessManagerActivity.class));
break;
case 5:
// 手机杀毒
startActivity(new Intent(getApplicationContext(),AnitVirusActivity.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;
}
}
});
}
在activity包下新建AnitVirusActivity,作为“手机杀毒”模块的页面,首先修改其布局文件activity_anit_virus.xml,按照之前的思路进行完善,代码如下:
<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.AnitVirusActivity"
android:orientation="vertical">
<TextView
style="@style/TitleStyle"
android:text="手机杀毒"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_scanner_malware"/>
<ImageView
android:id="@+id/iv_scanning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/act_scanning_03"/>
RelativeLayout>
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_name"
android:text="正在扫描应用:"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ProgressBar
android:id="@+id/pb_bar"
android:progressDrawable="@drawable/progress_bg"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
LinearLayout>
LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/ll_add_text"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
LinearLayout>
ScrollView>
LinearLayout>
注意,由于进度条使用自定义样式,为了维护其样式需要填充三张图片,在res/drawable下新建一个名为progress_bg.xml的layer-list
文件,代码如下:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@drawable/security_progress_bg"/>
<item android:id="@android:id/secondaryProgress" android:drawable="@drawable/security_progress"/>
<item android:id="@android:id/progress" android:drawable="@drawable/security_progress"/>
layer-list>
上一小节中,我们完成了“手机杀毒”模块的布局实现,现在需要完善病毒数据库的查询过程。判断一个应用是否为病毒,只需要从数据库从提取信息并进行字段(MD5码)比对即可确认。
修改SplashActivity,修改initDB()方法,在其中使用initAddressDB()方法,准备病毒数据库的信息录入,以此为查询过程做准备,代码如下:
/**
* 12.初始化数据库
*/
private void initDB(){
// 归属地数据拷贝过程
initAddressDB("address.db");
// 常用号码数据库的拷贝过程
initAddressDB("commonnum.db");
// 病毒数据库的拷贝过程
initAddressDB("antivirus.db");
}
在dao下新建VirusDao,作为操作数据库中数据表里数据的工具类,增加常用方法,代码如下:
package com.example.mobilesafe.dao;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;
import java.util.List;
public class VirusDao {
// 1.指定访问数据库的路径
public static String path = "data/data/com.example.mobilesafe/files/antivirus.db";
// 2.开启数据库,查询数据库中表对应的md5码
public static List<String> getVirusList(){
SQLiteDatabase db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
Cursor cursor = db.query("datable", new String[]{"md5"}, null, null, null, null, null);
ArrayList<String> virusList = new ArrayList<>();
while (cursor.moveToNext()){
String strMd5 = cursor.getString(0);
virusList.add(strMd5);
}
cursor.close();
db.close();
return virusList;
};
}
在进行数据操作前,我们先来完善该模块下界面的一些动画逻辑 。
修改AnitVirusActivity,完善旋转动画的逻辑,代码如下:
package com.example.mobilesafe.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.example.mobilesafe.R;
public class AnitVirusActivity extends AppCompatActivity {
private ImageView iv_scanning;
private TextView tv_name;
private ProgressBar pb_bar;
private LinearLayout ll_add_text;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anit_virus);
// 初始化UI
initUI();
// 初始化动画
initAnimation();
}
/**
* 初始化UI
*/
private void initUI() {
iv_scanning = findViewById(R.id.iv_scanning);
tv_name = findViewById(R.id.tv_name);
pb_bar = findViewById(R.id.pb_bar);
ll_add_text = findViewById(R.id.ll_add_text);
}
/**
* 初始化动画
*/
private void initAnimation() {
RotateAnimation rotateAnimation= new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(1000);
// 指定动画一直旋转
rotateAnimation.setRepeatCount(RotateAnimation.INFINITE);
// 指定旋转后的位置,保持动画执行结束后的状态
rotateAnimation.setFillAfter(true);
// 执行旋转动画
iv_scanning.startAnimation(rotateAnimation);
}
}
修改AnitVirusActivity,添加checkVirus(),用于遍历所有应用然后判断应用是否为病毒,代码如下:
package com.example.mobilesafe.activity;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.example.mobilesafe.R;
import com.example.mobilesafe.dao.VirusDao;
import com.example.mobilesafe.domain.ScanInfo;
import com.example.mobilesafe.utils.MD5Util;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class AnitVirusActivity extends AppCompatActivity {
private static final int SCANING = 100;
private static final int SCANING_FINISH = 101;
private ImageView iv_scanning;
private TextView tv_name;
private ProgressBar pb_bar;
private LinearLayout ll_add_text;
private int index = 0;
private List<ScanInfo> virusScanInfoList;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case SCANING:
// 1.显示正在扫描应用的名称
ScanInfo scanInfo = (ScanInfo) msg.obj;
tv_name.setText(scanInfo.getName());
// 2.在线性布局中添加一个正在扫描应用的TextView
TextView textView = new TextView(getApplicationContext());
if (scanInfo.isVirus()){
// 是病毒
textView.setTextColor(Color.RED);
textView.setText("发现病毒:" + scanInfo.getName());
}else {
// 不是病毒
textView.setTextColor(Color.BLACK);
textView.setText("扫描安全:" + scanInfo.getName());
}
ll_add_text.addView(textView,0); // 索引为0代表每次都在最上面添加
break;
case SCANING_FINISH:
tv_name.setText("扫描完成");
iv_scanning.clearAnimation(); // 停止在控件上正在执行的旋转
// 告知用户要去卸载包含了病毒的应用
uninstallVirus();
break;
}
}
};
/**
* 卸载包含了病毒的应用
*/
private void uninstallVirus() {
for (ScanInfo scanInfo : virusScanInfoList) {
String packageName = scanInfo.getPackageName();
// 隐式Intent启动卸载功能
Intent intent = new Intent("android.intent.action.DELETE");
intent.addCategory("android.intent.category.DEFAULT");
intent.setData(Uri.parse("package:" + packageName));
startActivity(intent);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anit_virus);
// 初始化UI
initUI();
// 初始化动画
initAnimation();
// 检测病毒
checkVirus();
}
/**
* 初始化UI
*/
private void initUI() {
iv_scanning = findViewById(R.id.iv_scanning);
tv_name = findViewById(R.id.tv_name);
pb_bar = findViewById(R.id.pb_bar);
ll_add_text = findViewById(R.id.ll_add_text);
}
/**
* 初始化动画
*/
private void initAnimation() {
RotateAnimation rotateAnimation= new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(1000);
// 指定动画一直旋转
rotateAnimation.setRepeatCount(RotateAnimation.INFINITE);
// 指定旋转后的位置,保持动画执行结束后的状态
rotateAnimation.setFillAfter(true);
// 执行旋转动画
iv_scanning.startAnimation(rotateAnimation);
}
/**
* 检测手机上的病毒
*/
private void checkVirus() {
new Thread(){
@Override
public void run() {
// 获取数据库中所有病毒的md5码
List<String> virusList = VirusDao.getVirusList();
// 获取手机上面的所有应用程序签名文件的md5码
// 1.获取包管理对象
PackageManager packageManager = getPackageManager();
// 2.获取所有应用程序所对应的签名文件,参数为(已安装应用的签名文件 + 卸载应用的残余文件)
List<PackageInfo> installedPackages = packageManager.getInstalledPackages(PackageManager.GET_SIGNATURES + PackageManager.GET_UNINSTALLED_PACKAGES);
// 记录病毒的List
virusScanInfoList = new ArrayList<>();
List<ScanInfo> scanInfoList= new ArrayList<>(); // 记录所有应用的List
// 设置进度条最大值
pb_bar.setMax(installedPackages.size());
// 3.遍历应用集合
for (PackageInfo packageInfo : installedPackages) {
ScanInfo scanInfo = new ScanInfo();
Signature[] signatures = packageInfo.signatures;
// 4.获取签名文件数组的第一位,然后进行md5算法转换,将此md5和数据库中的md5进行比对
Signature signature = signatures[0];
String s = signature.toCharsString();
// 32位字符串,16进制字符(0-f)
String encoder = MD5Util.encoder(s);
// 5.比对应用是否为病毒
if (virusList.contains(encoder)){
// 6.记录病毒
scanInfo.setVirus(true);
virusScanInfoList.add(scanInfo);
}else {
scanInfo.setVirus(false);
}
// 7.维护对象的包名,以及应用名称
scanInfo.setPackageName(packageInfo.packageName);
scanInfo.setName(packageInfo.applicationInfo.loadLabel(packageManager).toString());
scanInfoList.add(scanInfo);
// 8.更新进度条的百分比
index++;
pb_bar.setProgress(index);
// 9.模拟耗时操作
try {
Thread.sleep(50 + new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
// 10.在子线程中发送消息,告知主线程更新UI(1:顶部扫描应用的名称,2:扫描过程中往线性布局添加view)
Message message = Message.obtain();
message.what = SCANING;
message.obj = scanInfo;
mHandler.sendMessage(message);
}
// 11.当扫描所有应用后,将文本置为“扫描完成”
Message message = Message.obtain();
message.what = SCANING_FINISH;
mHandler.sendMessage(message);
}
}.start();
}
}
在domain包下新建ScanInfo,作为记录扫描信息的实体类,代码如下:
package com.example.mobilesafe.domain;
public class ScanInfo {
private boolean isVirus;
private String packageName;
private String name;
public boolean isVirus() {
return isVirus;
}
public void setVirus(boolean virus) {
isVirus = virus;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}