#11.1 复习 #
11.2 解锁的操作 # (发送广播的操作、服务中注册广播接收广播事件)
问题:
先打开APIDemos项目,打开之后直接弹出了解锁界面,然后点击模拟器的返回键退出了APIDemos项目
然后紧接着打开了手机卫士,打开手机卫士之后,什么情况,又弹出了APIDemos项目中的解锁界面了
这个问题很严重,打开手机卫士时,应该是手机卫士界面
原因:
打开APIDemos项目直接弹出了解锁界面,这个解锁APIDemos界面是属于手机卫士的界面
现在退出这个解锁界面,当退出APIDemos项目时,只是退出了APIDemos项目,这个解锁界面没有退出只是不可见了
当再打开手机卫士时,这个手机卫士里边包含这个解锁(输入密码)界面,那就会直接弹出来
解决:
找到WatchDogActivity,既然你在退出这个APIDemos时,这个WatchDogActivity也不可见了
那我在它不可见时在WatchDogActivity的onStop方法中调用finish()方法,将它finish掉
@Override
protected void onStop() {
super.onStop();
finish();
}
运行程序,勾选手机卫士项目中的设置中心条目中的软件锁来开启软件锁,然后返回到桌面,再打开APIDemos项目直接弹出了解锁(输入密码)界面,然后点击返回键回到桌面,点击手机卫士项目直接进入了手机卫士主界面,就不会弹出解锁(输入密码)界面
弹出输入密码框的功能做完了,接下来就应该完成下解锁操作
在打开APIDemos项目时,直接弹出解锁(输入密码)界面,然后输入密码点击解锁来解锁了,密码正确退出WatchDogActivity界面
要输入密码得要先设置密码,设置密码在前边讲手机防盗时讲过,这里就不说了,简单点直接把这个密码写死
直接讲下解锁的操作
首先要获取下输入的密码,然后在解锁里边判断密码是否正确
来到布局文件activity_watchdog.xml,先给输入框设置一个id为ed_watchdog_password
紧接着给这个Button按钮设置一个点击事件的属性为:
android:onClick="unlock"
先把这个输入密码框的id在WatchDogActivity的onCreate方法中初始化出来,
接下来在WatchDogActivity中创建点击方法unlock,在unlock方法中首先要获取输入的密码,
接下来就可以判断密码是否正确,如果"123".equals(password),注意这样写是为了防止空指针异常,如果密码正确就可以去关闭输入密码界面了,else提醒用户“密码错误”,关闭输入密码界面很简单了,就一个finish
运行程序,为了方便测试先把APIDemos退出
打开手机卫士中设置中心条目中的软件锁,退出到桌面,打开APIDemos项目会弹出输入密码界面,输入123点击解锁
又回到输入密码界面了
输入123点击解锁,执行了finish()方法关闭了输入密码界面了,然后应该进入APIDemos项目的主界面
原因:
在WatchDogService服务中判断时,是时时刻刻在监听当前打开的应用程序在数据库中有没有包名
输入密码123是解锁了,但紧接着判断发现打开的还是APIDemos应用程序,数据库中有APIDemos应用程序的包名,那它又会走WatchDogService.java中:
//6.判断包名是否在数据库中
if (islock) {
if (!packageName.equals(unlockPackageName)) {
//跳转到密码输入界面
Intent intent = new Intent(WatchDogService.this,WatchDogActivity.class);
//给跳转到的activity指定一个任务栈
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//将包名传递给密码输入界面,用于显示图标和名称
intent.putExtra("packagename", packageName);
startActivity(intent);
}
}
所以会重新去打开输入密码框界面
接下来这么干,输入密码123点击解锁之后,要通知WwatchDogService服务当前的应用不用再去加锁了
要让activity(WatchDogActivity)给服务service(WatchDogService)传递数据,有些同学想到用startActivityForResult
startActivityForResult是activity给activity传递数据时用
activity给service传递数据一般会发送广播来处理
发送广播:
先new个Intent
用intent去setAction设置要发送的广播事件
广播事件我们写成"content://cn.itcast.mobliesafexian02.unlock"
来表示这个广播要进行解锁操作
用sendBroadcast(intent)去发送广播
但是注意,我们要告诉服务WatchDogService当前的这个应用不能加锁,那服务怎么知道现在的应用是当前的应用?
可以用intent去putExtra("packagename", packagename),传一个我们这个packagename过去,找一下packagename
在WatchDogActivity中的“1.获取传递过来的包名处”,即:
//1.获取传递过来的包名
packagename = intent.getStringExtra("packagename");
WatchDogActivity中的packagename就是从服务WatchDogService里边传递过来的,服务传递过来的包名,说明服务检测到当前的应用是当前打开的应用,接下来给当前的应用输入密码进行解锁之后,再把包名给服务WatchDogService
即WatchDogActivity的unlock方法中的intent.putExtra参数中写成packagename,就表示这个包名是打开的应用已经解锁了
public void unlock(View v){
//获取输入的密码
String password = ed_watchdog_password.getText().toString().trim();
//判断密码是否正确
if ("123".equals(password)) {
//关闭输入密码界面
//发送广播
Intent intent = new Intent();
//发送的广播事件
intent.setAction("content://cn.itcast.mobliesafexian02.unlock");
intent.putExtra("packagename", packagename);
sendBroadcast(intent);
finish();
}else{
Toast.makeText(getApplicationContext(), "密码错误", 0).show();
}
}
上边WatchDogActivity中发送的广播,是准备发送给服务WatchDogService的,所以需要在服务WatchDogService中注册一个广播用来接收WatchDogActivity中发送的广播事件
那接下来在服务WatchDogService里边注册一个广播把它叫做UnlockReceiver,并重写onReceive方法
然后在WatchDogService的onCreate方法中注册这个广播接收者
先new出来一个UnlockReceiver,然后设置过滤条件,先new出来一个IntentFilter
用intentFilter调用addAction去添加接收的广播事件
addAction中接收的广播事件要与WatchDogActivity中发送的广播事件一模一样
即:"content://cn.itcast.mobliesafexian02.unlock"
用registerReceiver(unlockReceiver, intentFilter)注册广播接受者
有注册就要有注销,去WatchDogService中的onDestroy方法中去注销广播接收者,和以前一样要判断一下,如果unlockReceiver不为null的话去注销广播接收者,同时将unlockReceiver置为null
在onReceive方法中就可以获取从WatchDogActivity中发送广播时通过intent.putExtra("packagename", packagename)传递过来的包名了
在onReceive方法的参数处看见有个intent参数,用intent去getStringExtra("packagename")
参数处写成WatchDogActivity的unlock方法中intent.putExtra("packagename", packagename)里边的双引号中的name
返回来一个Sting类型的包名,我们叫做unlockPackageName,把它设置成成员变量
public class WatchDogService extends Service {
private UnlockReceiver unlockReceiver;
private String unlockPackageName;
@Override
public void onCreate() {
super.onCreate();
//注册广播接受者
unlockReceiver = new UnlockReceiver();
//设置过滤条件
IntentFilter intentFilter = new IntentFilter();
//设置接受的广播事件
intentFilter.addAction("content://cn.itcast.mobliesafexian02.unlock");
registerReceiver(unlockReceiver, intentFilter);
}
private class UnlockReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
//获取传递过来的包名
unlockPackageName = intent.getStringExtra("packagename");
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (unlockReceiver != null) {
unregisterReceiver(unlockReceiver);
unlockReceiver = null;
}
}
}
这个传递过来的包名unlockPackageName就是解锁之后要打开的应用程序的包名
原来判断包名是否在数据库中的操作如下(在WatchDogService.java中)
有这个包名之后,需要再给它添加一个判断,如果!packageName.equals(unlockPackageName)
也就是如果你的包名不等于我传递过来的包名,那你就去进行加锁操作,但是如果你的包名等于我传递过来的包名,就表明当前我这个应用程序要进行解锁操作,你就不要再去监听我这个什么时候打开再给我加锁了
//6.判断包名是否在数据库中
if (watchDogDao.queryLockApp(pachageName)) {
if (!packageName.equals(unlockPackageName)) {
//跳转到密码输入界面
Intent intent = new Intent(WatchDogService.this,WatchDogActivity.class);
//给跳转到的activity指定一个任务栈
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//将包名传递给密码输入界面,用于显示图标和名称
intent.putExtra("packagename", packageName);
startActivity(intent);
}
}
运行程序,勾选手机卫士设置中心的软件锁开启软件锁,退出到桌面,再打开APIDemos项目,直接进入了输入密码界面,输入123点击解锁
就进入到APIDemos项目的主页了,就没有再弹出解锁界面了,那现在退出APIDemos,再点击APIDemos进去,也不会再打开输入密码界面了
点击模拟器的电源键,模拟器锁屏了,向右滑动解锁,然后再点击APIDemos项目直接进入了APIDemos项目的主界面
解锁一次后边不管什么时候进去都不会再弹出解锁界面了
那这是一个问题,一般锁屏之后再解锁进入桌面,点击APIDemos项目进去时,应该让它还要进行解锁操作
开发中我们会这么认为,用户只要进行解锁APIDemos的操作,手机没有锁屏时,一般就认为用户一直在用这个APIDemos,就不需要再进行解锁APIDemos的操作,但是如果手机锁屏了,就表明不再使用这个APIDemos了,那当你再次解锁手机,打开APIDemos项目时,就还要让你去输入密码进行解锁
要监听手机锁屏时,重新给这个APIDemos上锁,那接下来要去注册一个锁屏的广播接收者,这个锁屏的广播接收者前边刚讲过
在WatchDogService的成员变量处,写一个ScreenOffReceiver的广播接收者,并重写它的onReceive方法,
然后在WatchDogService的onCreate方法中,注册ScreenOffReceiver这个锁屏广播接收者
和注册解锁的广播接收者一样,先new出来ScreenOffReceiver,然后再new一个IntentFilter,通过screenIntentFilter去
addAction来设置我们接收的广播事件,这个里边有一个锁屏的操作Intent.ACTION_SCREEN_OFF,最后要用registerReceiver(screenOffReceiver, screenIntentFilter)去注册广播接收者
有注册就有注销,别忘了在WatchDogService的onDestroy方法中注销这个ScreenOffReceiver广播接收者
锁屏的广播接收者也做完了,在判断是否加锁时上边添加了新判断,看传递过来的包名是否和当前的包名一致,如果不一致就弹出输入密码界面,那在ScreenOffReceiver的onReceive方法中可以这么干,当你锁屏时,把unlockPackageName置为null或是空字符串"",
传个null是肯定没有问题,看下:
if (!packageName.equals(unlockPackageName)) {
专门把packageName放到前边,就是为了避免空指针异常
public class WatchDogService extends Service {
private UnlockReceiver unlockReceiver;
private String unlockPackageName;
private ScreenOffReceiver screenOffReceiver;
public class ScreenOffReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
unlockPackageName = null;
}
}
@Override
public void onCreate() {
super.onCreate();
//注册广播接受者
unlockReceiver = new UnlockReceiver();
//设置过滤条件
IntentFilter intentFilter = new IntentFilter();
//设置接受的广播事件
intentFilter.addAction("content://cn.itcast.mobliesafexian02.unlock");
registerReceiver(unlockReceiver, intentFilter);
//注册锁屏的广播接受者
screenOffReceiver = new ScreenOffReceiver();
//设置过滤条件
IntentFilter screenIntentFilter = new IntentFilter();
screenIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(screenOffReceiver, screenIntentFilter);
}
private class UnlockReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
//获取传递过来的包名
unlockPackageName = intent.getStringExtra("packagename");
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (unlockReceiver != null) {
unregisterReceiver(unlockReceiver);
unlockReceiver = null;
}
if (screenOffReceiver != null) {
unregisterReceiver(screenOffReceiver);
screenOffReceiver=null;
}
}
}
运行程序, 勾选上手机卫士设置中心条目中的软件锁,开启软件锁,然后退出到桌面,打开APIDemos项目,会弹出解锁的界面,输入123点击解锁,就进入到APIDemos项目的主界面了
现在点击模拟器的电源按钮,让模拟器锁屏,然后向右滑动解锁模拟器,再点击APIDemos项目,又弹出输入密码界面了, 再输入123点击解锁,就进去APIDemos项目的主界面了
解锁的操作步骤总结如下:
1.给button增加点击事件,在点击事件中判断密码输入是否正确,正确,finish输入密码界面,不正确提醒用户
2.发现解锁之后,会重复显示输入密码界面
原因:在服务中不断在监听当前打开的应用程序,判断包名在数据库中有没有,如果有 就重新走跳转到密码输入界面,所以就会不断弹出输入密码界面,解决办法:广播 (解锁后通知当前应用,不用再给加锁了,让activity给服务传递数据,有些同学会想到用startActivityforResult,但它是activity给activity传递数据用的,现在是activity给service传递数据,这个时候一般会用broadcast)
a.在点击事件中 增加发送广播操作
b.在服务中 注册广播进行接收
3.增加锁屏之后,解锁,再次显示输入密码操作
问题:打开APIDemos时,弹出解锁密码,点击返回键退出APIDemos,进入手机卫士时,本来应该显示手机卫士界面,界面却变成APIDemos的解锁密码界面
解决:在退出API Demos时,API Demos的界面不可见了onStop(),让API Demos的解锁密码也不可见finish掉即可
在Watch Dog Activity.java中添加方法如下:
protected void onStop(){
super.onStop();
//让API Demos的解锁密码不可见
finish();
}
11.3 bug处理 # (重点)
解锁功能处理完了,但还有一些问题
问题1:
点击模拟器的电源按钮让模拟器锁屏,然后向右滑动解锁,勾选上手机卫士设置中心条目中的软件锁,以前都是点击模拟器的物理退出按钮退出,现在不退出了,点击HOME键,然后打开APIDemos项目,弹出了输入密码界面,输入123点击解锁,弹出的居然是手机卫士的设置中心界面,此时再点击模拟器的物理退出按钮,退出到手机卫士主界面了,再点击退出按钮才是APIDemos项目的主界面
原因:
画下图,给大家走下流程,打开的每一个应用程序都有任务栈,模拟器的桌面(桌面也是一个应用程序)是桌面任务栈它叫launcher
打开桌面的手机卫士,当前的任务栈就是手机卫士的任务栈了,那手机卫士的任务栈是在laucher栈的前边了
在手机卫士的任务栈中首先有个homeactivity,然后打开了手机卫士设置中心界面了,这时又会添加SettingActivity到手机卫士的任务栈
这时点击了home键,回到手机桌面了,相当于这个launcher任务栈又跑前边来了
这时打开APIDemos项目会弹出输入密码界面,输入密码界面属于手机卫士,相当于弹出了APIDemos项目的任务栈
但是打开APIDemos任务栈时,会弹出WatchDogActivity,WatchDogActivity属于手机卫士
那在手机卫士的任务栈中又来了一个WatchDogActivity
现在看到的是watchdogactivity,那这表明手机卫士的任务栈又跑到前边了
那这时候输入密码123点击解锁,把watchdogActivity给finish掉了
那手机卫士任务栈栈顶是settingActivity,刚好我们看见弹出的就是settingActivity
那再把settingActivity退出,手机卫士任务栈栈顶就是homeactivity了
再把homeactivity退出,手机卫士的任务栈中没有activity了,相当于把手机卫士整个应用给退出了
整个应用退出了就显示出apidemos(apidemos的任务栈)了
这就是为什么当点击解锁之后会弹出手机卫士的设置中心的原因
解决:
当点击apidemos时,弹出了watchdogActivity,这个watchdogactivity就直接放到了手机卫士的任务栈当中了
那让这个watchdogactivity单独占一个任务栈
到手机卫士清单文件中注册WatchDogActivity处,给它添加属性launchMode
里边有四种启动模式:
standard: 默认的启动模式,就是平时怎么启动,就怎么启动
singleTop:如果这个activity在任务栈顶,就不去创建新的了
singleTask:如果任务栈中有这个activity,调用时,就会把它上边的其他activity给移除掉
singleInstance:单独给一个activity设置一个任务栈
面试会问,都是基础性的知识,千万不要忘记,面试时候就是专门问你忘了的知识来判断你基础知识扎实不扎实
在清单文件中单独给WatchDogActivity一个任务栈
即:
<activity
android:name=".WatchDogActivity"
android:launchMode="singleInstance" >
</activity>
运行程序,勾选上手机卫士的设置中心的软件锁,点击home键来到桌面,再点击APIDemos,弹出了输入密码界面,输入123然后解锁
就直接进入了APIDemos的主界面
这个问题就解决了
再来给大家画个图说下
首先是laucher界面,那首先打开的就是launcher任务栈,然后打开了手机卫士的界面,此时打开了手机卫士的任务栈
手机卫士的任务栈中有homeactivity,settingActivity,这时候点击home键,把手机卫士给最小化
然后打开apidemos,那apidemos任务栈就跑到最前边了,打开apidemos项目,会弹出watchdogactivity
因为在清单文件中给watchdogactivity设置了launcherMode启动模式为singleInstance
给watchdogactivity单独设置了一个任务栈,那这时会在apidemos任务栈的前边再来一个任务栈
这个任务栈就是watchdogactivity的任务栈,它这里边就只有一个watchdogactivity
然后点击解锁之后,退出了watchdogactivity以及watchdogactivity的任务栈,那这时弹出的就是apidemos的任务栈
这时就来到了apidemos的主界面,就不会再弹出手机卫士的设置界面
问题2:
先点击模拟器的电源按钮加锁,然后向右滑动解锁,点击apidemos,要输入密码,输入123点击解锁,进入到apidemos项目主界面了,长按home键,下边有一个手机卫士的图标和名称,那点击它,没有展示手机卫士界面,居然弹出的是解锁界面
看下腾讯卫士怎么做,点击腾讯卫士,点击隐私保护,再点击软件锁,输入密码123点击确定,然后看下已加锁它里边有个APIDemos
那点击模拟器的返回按钮一步一步退出腾讯卫士到手机桌面,然后打开apidemos,会弹出腾讯卫士的解锁界面,输入123点击确定
现在解锁了,接下来长按home键弹出最近列表,它里边有这个腾讯卫士吗?没有,这是腾讯卫士为了避免这个bug出现做的一个小操作
其实是把腾讯卫士不让显示在最近列表中
来到手机卫士清单文件的注册watchdogactivity处,给它添加excludeFromRecents属性,意思是不在最近列表显示,设置成true
<activity
android:name=".WatchDogActivity"
android:excludeFromRecents="true"
android:launchMode="singleInstance" >
</activity>
打开apidemos,输入123点击解锁,此时进入到了apidemos的主界面,再长按home键,在最近列表中就没有手机卫士了
excludeFromRecents属性是不在最近列表中显示,true表示不显示,false表示显示,但是在哪个activity中设置,哪个acitivity调用之后才会生效
11.4 解锁的数据库优化操作 # (重点)
1.将数据库中数据全部放到集合中,改为从集合中查询数据
重新审视watchdogservice代码,发现“2.开启线程时时刻刻监听用户打开的应用程序”,并且在下边做了判断“6.判断包名是否在数据库中”,一直在查询数据库,即:
if(watchDogDao.queryLockApp(packageName)){
多久去查询一次?即:
SystemClock.sleep(500);
每隔500毫秒查询一次数据库,点击queryLockApp来到WatchDogDao的queryLockApp方法处,看到查询数据库时做的操作是:
获取数据库(getReadableDatabase),然后一直查询(query),再关闭数据库(close)
每五百毫秒打开关闭一次数据库,是很耗费系统资源(cpu资源)的
1)查询全部数据返回到集合List中
前边在WatchDogDao中写数据库操作时还写过一个getAllLockApp方法,它是查询全部的操作
到WatchDogService中的new Thread的run方法中,用WatchDogDao调用getAllLockApp查询全部,返回一个List集合,取名lockApps,抽取成成员变量
2)list.contains(packagename)判断是否加锁
获取出全部数据之后,用lockApps调用contains(object),参数object写成获取到的应用程序的包名packageName
表示判断这个包名是否包含在集合lockApps中,返回一个boolean类型的islock
3)将频繁查询数据库的代码改用boolean类型的islock
如果包含就表示它已经加锁,反之未加锁
这样写始终只查询了一次数据库(即watchDogDao.getAllLockApp()),之后就将集合中的数据和打开的应用程序的包名进行判断(看从数据库中获取到集合中的数据中有没有打开的应用程序的包名),这样就减少打开关闭数据库的次数了
开发中经常做的操作:
当遇到需要频繁打开关闭数据库的操作时,把数据库中的所有数据一次获取出来存放到集合中(集合在内存中,所以也叫放到内存中)
将之前频繁查询数据库的操作改为将数据库中的数据全部放到内存中,然后从内存中去查询,从内存中查询比数据库中查询少消耗很多资源
private List<String> lockApps;
//2.开启线程时时刻刻监听用户打开的应用程序
new Thread(){
public void run() {
//将数据库中的所有数据存放到集合中,也就是存放到内存中
lockApps = watchDogDao.getAllLockApp();
while(isTasks){
//3.获取正在的运行任务栈
//maxNum : 获取前几个正在运行的任务栈
List<RunningTaskInfo> runningTasks = am.getRunningTasks(1);
for (RunningTaskInfo runningTaskInfo : runningTasks) {
//4.获取栈底的activity
ComponentName baseactivity = runningTaskInfo.baseActivity;
// runningTaskInfo.topActivity;//获取栈顶的activity
//5.获取应用程序的包名
String packageName = baseactivity.getPackageName();
//查看list集合中是否包含包名
boolean islock = lockApps.contains(packageName);
//6.判断包名是否在数据库中
if (islock) {
if (!packageName.equals(unlockPackageName)) {
//跳转到密码输入界面
Intent intent = new Intent(WatchDogService.this,WatchDogActivity.class);
//给跳转到的activity指定一个任务栈
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//将包名传递给密码输入界面,用于显示图标和名称
intent.putExtra("packagename", packageName);
startActivity(intent);
}
}
System.out.println(packageName);
}
SystemClock.sleep(500);
}
};
}.start();
运行程序,打开设置中心中的软件锁功能,然后退出到桌面,打开APIDemos,输入123点击解锁,也没有问题
2.更新集合,在加锁和解锁时都要更新集合,用到内容观察者
APIDemos是以前设置的加锁,打开手机卫士西安02,打开软件管理,长按“三击”项目加锁,退出到桌面
按理来说点击“三击”项目时,应该弹出输入密码界面,但点击之后并没有弹出来
刚才把数据库中的所有数据存放到了集合(内存)中会导致在集合中查询数据时,三击已经加锁了但是集合中的数据还没有更新
出现打开“三击”项目时不会弹出输入密码界面的问题
所以在加锁或解锁时需要更新集合,这里就要用到内容观察者
1)注册一个内容观察者
在刚才WatchDogService中的子线程的run方法实现内容观察者代码
用内容解析者getContentResolver()调用registerContentObserver(uri,notifyForDescendents,observer)
参1:需要一个uri,uri还没有,先不管它
参2:需要一个匹配,true或false都可以,
参3:需要一个内容观察者observer,new一个内容观察者即new ContentObserver(handler)
参数handler:需要new一个handler出来
在它里边实现onChange方法,内容观察者监听到数据变化时会调用onChange方法
2)在内容观察者的onChange方法中查询全部数据到集合
在onChange方法中来实现更新数据的操作,还是查询数据库中全部数据的操作,更新数据库数据到集合(内存)中的操作
即:
//2.开启线程时时刻刻监听用户打开的应用程序
new Thread(){
public void run() {
//加锁或者解锁的时候要更新集合,内容观察者,查看到数据变化的时候会调用onChange方法
Uri uri = Uri.parse("content://cn.itcast.mobliesafexian02.unlock.change");
//更新操作
getContentResolver().registerContentObserver(uri, true, new ContentObserver(null) {
public void onChange(boolean selfChange) {
//更新数据
lockApps = watchDogDao.getAllLockApp();
};
});
//将数据库中的所有数据存放到集合中,也就是存放到内存中
lockApps = watchDogDao.getAllLockApp();
while(isTasks){
//3.获取正在的运行任务栈
//maxNum : 获取前几个正在运行的任务栈
List<RunningTaskInfo> runningTasks = am.getRunningTasks(1);
for (RunningTaskInfo runningTaskInfo : runningTasks) {
//4.获取栈底的activity
ComponentName baseactivity = runningTaskInfo.baseActivity;
// runningTaskInfo.topActivity;//获取栈顶的activity
//5.获取应用程序的包名
String packageName = baseactivity.getPackageName();
//查看list集合中是否包含包名
boolean islock = lockApps.contains(packageName);
//6.判断包名是否在数据库中
if (islock) {
if (!packageName.equals(unlockPackageName)) {
//跳转到密码输入界面
Intent intent = new Intent(WatchDogService.this,WatchDogActivity.class);
//给跳转到的activity指定一个任务栈
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//将包名传递给密码输入界面,用于显示图标和名称
intent.putExtra("packagename", packageName);
startActivity(intent);
}
}
System.out.println(packageName);
}
SystemClock.sleep(500);
}
};
}.start();
3)加锁解锁时通知内容观察者内容变化了
接下来要判断当加锁或解锁时,通知内容观察者内容变化了,加锁解锁操作在哪里做比较合适?
加锁时会通过WatchDogDao中的addLockApp方法添加对应包名到数据库中,添加成功就告诉内容观察者数据变化了
那要在WatchDogDao中的addLockApp方法写代码
在WatchDogDao的构造方法里边通过this.context = context获取到这个context,并声明成成员变量
然后用context去获取一个内容解析者getContentResolver(),返回一个内容解析者ContentResolver,取名resolver
用resolver调用notifyChange(uri,observer),这个方法就是通知内容观察者数据已发生变化
参数uri:需要一个uri
我们自己通知内容观察者,然后内容观察者自己接收发送过来的内容是否有更新的数据
那这个uri就需要自己去写,凡是和内容解析者和提供者相关的,协议都是content再加上cn.itcast.mobilesafexian02
这个功能是用来观察解锁数据是否发生变化,所以再追加一个unlock.change,即:
Uri uri = Uri.parse("content://cn.itcast.mobliesafexian02.unlock.change");
参observer:需要一个内容观察者,直接设置为null
要明白为什么这里要设置成null,因为我们内容观察者是在我们服务WatchDogService中已经写好了
private Context context;
public WatchDogDao(Context context){
this.context = context;
watchDogOpenHelper = new WatchDogHelper(context);
}
/**
-
添加包名
*/
public void addLockApp(String packagename){
// synchronized (b) {
// }//1.获取数据库 // blacNunOpenHelper.getReadableDatabase();//也是可以进行写入操作的,不加锁,线程不安全的操作,效率比较高,读取数据库用,对数据库进行操作不会使用 SQLiteDatabase database = watchDogOpenHelper.getWritableDatabase();//加锁,线程安全的,效率比较低,一般用于对数据库进行操作使用 //跟map相似 ContentValues values = new ContentValues(); values.put("packagename", packagename); //nullColumnHack : sqlit数据库是不允许字段出现null //values : 要添加的数据 database.insert(WatchDogOpenHelper.DB_NAME, null, values); //告诉内容观察者,数据已经变化,可以去更新数据 ContentResolver resolver = context.getContentResolver(); Uri uri = Uri.parse("content://cn.itcast.mobliesafexian02.unlock.change"); //通知内容观察者,数据已经发生变化了 resolver.notifyChange(uri, null); database.close(); }
在WatchDogDao中的addLockApp方法这里我通知内容观察者内容发生变化
然后复制WatchDogDao中的addLockApp中自己写的uri到服务WatchDogService中的uri中并设置给参数处,然后用内容观察者去更新数据
//2.开启线程时时刻刻监听用户打开的应用程序 new Thread(){ public void run() { //加锁或者解锁的时候要更新集合,内容观察者,查看到数据变化的时候会调用onChange方法 Uri uri = Uri.parse("content://cn.itcast.mobliesafexian02.unlock.change"); //更新操作 getContentResolver().registerContentObserver( uri, true, new ContentObserver(null) { public void onChange(boolean selfChange) { //更新数据 lockApps = watchDogDao.getAllLockApp(); }; }); //将数据库中的所有数据存放到集合中,也就是存放到内存中 lockApps = watchDogDao.getAllLockApp(); while(isTasks){ //3.获取正在的运行任务栈 //maxNum : 获取前几个正在运行的任务栈 List<RunningTaskInfo> runningTasks = am.getRunningTasks(1); for (RunningTaskInfo runningTaskInfo : runningTasks) { //4.获取栈底的activity ComponentName baseactivity = runningTaskInfo.baseActivity;
// runningTaskInfo.topActivity;//获取栈顶的activity
//5.获取应用程序的包名 String packageName = baseactivity.getPackageName(); //查看list集合中是否包含包名 boolean islock = lockApps.contains(packageName); //6.判断包名是否在数据库中 if (islock) { if (!packageName.equals(unlockPackageName)) { //跳转到密码输入界面 Intent intent = new Intent(WatchDogService.this,WatchDogActivity.class); //给跳转到的activity指定一个任务栈 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //将包名传递给密码输入界面,用于显示图标和名称 intent.putExtra("packagename", packageName); startActivity(intent); } } System.out.println(packageName); } SystemClock.sleep(500); } }; }.start();
运行程序,点击设置中心,报错了,点击报错的地方,跳转到WatchDogService中的82行
getContentResolver().registerContentObserver( uri, true, new ContentObserver(new Handler()) {
原因是在WatchDogDao中的第50行:
//通知内容观察者,数据已经发生变化了 resolver.notifyChange(uri, null);
在参数observer这里传了null,当我们在watchDogService中再写内容观察者ContentObserver时就不能写成new Handler了,也要写成null
getContentResolver().registerContentObserver( uri, true, new ContentObserver(null) {
原因:
在WatchDogService中new内容观察者ContentObserver时,看到它需要一个handler
handler是用来处理子线程更新主线程UI界面用的,这里可以通过handler去做一些其他处理
但是在WatchDogDao的resolver.notifyChange(uri,null)提醒内容观察者内容发生变化了
给WatchDogService中传递uri时,就写了null,告诉内容观察者不需要再进行任何处理了,只需要通过uri去更新数据
运行程序, 在手机卫士西安02的设置中心中开启软件锁,退出到桌面,重新进入手机卫士西安02,进入软件管理
长按“振动”项目,添加软件锁,退出到桌面,点击“振动”app,可以弹出输入密码界面了,123输入密码之后,就进入到振动的主界面了
给应用程序加锁的功能实现了,那再看下解锁操作,来到手机卫士西安02的软件管理界面,长按“小火箭”app
它的锁图标由加锁的图标变成了打开的一把锁图标了,退出到桌面,点击小火箭app,它还弹出解锁界面
那解锁还没有通知数据有变化
将watchDogDao中addLockApp方法中的如下代码:
//告诉内容观察者,数据已经变化,可以去更新数据 ContentResolver resolver = context.getContentResolver(); Uri uri = Uri.parse("content://cn.itcast.mobliesafexian02.unlock.change"); //通知内容观察者,数据已经发生变化了 resolver.notifyChange(uri, null);
拷贝到WatchDogDao中的删除包名deleteLockApp方法中,当删除成功时去通知内容观察者数据已经发生变化了
/**
-
删除包名
*/
public void deleteLockApp(String packagename){
//1.获取数据库
SQLiteDatabase database = watchDogOpenHelper.getWritableDatabase();
//2.删除操作
database.delete(WatchDogOpenHelper.DB_NAME, “packagename=?”, new String[]{packagename});//告诉内容观察者,数据已经变化,可以去更新数据 ContentResolver resolver = context.getContentResolver(); Uri uri = Uri.parse("content://cn.itcast.mobliesafexian02.unlock.change"); //通知内容观察者,数据已经发生变化了 resolver.notifyChange(uri, null); //3.关闭数据库 database.close(); }
运行程序,点击设置中心,将软件锁功能打开,然后到软件管理中长按“三击”,锁图标由加锁变成了解锁,然后退出到桌面
点击“三击”,进入了三击的主界面,没有再弹出输入密码界面了,这个就是通过内容观察者去处理的操作
总结:
第1步,主要是通过notifyChange方,通过uri去处理,比如内容发生变化了就会通过uri去通知内容观察者数据发生变化了
第2步,调用内容观察者执行更新操作,主要通过getContentResolver得到一个内容解析者,然后注册一个内容观察者,在onChange方法中进行更新数据的操作
注意,为什么知道数据库发生变化了?原因是这个uri起作用的
工作中:
一般不会这么频繁的操作数据库,如果像这里每500毫秒就频繁的查询(打开关闭)数据库 就必须用这种集合+观察者模式的方式去处理 因为频繁的打开关闭数据库,时间长了会出现ANR异常或者崩溃各种问题
11.5 获取短信的操作 #
接下来要做的都是小功能比较零碎,没有什么关联
金山卫士里边有一个短信管理,里边有个备份短信/还原短信功能
备份短信/还原短信:
短信备份,就是从手机中把短信全部读取出来保存到数据库或文件中
还原短信,就是从数据库或文件中,把短信拿出来添加到手机系统中
1.在手机卫士西安02的高级工具条目中增加这个功能
到布局文件activity_atool.xml中,给它增加两个button按钮(备份短信,还原短信)
activity_atool.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="高级工具"
android:textSize="25sp"
android:gravity="center_horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="#8866ff00"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查询号码归属地"
android:background="@drawable/selector_contact_button"
android:drawableLeft="@android:drawable/ic_menu_edit"
android:onClick="address"
/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="短信备份"
android:background="@drawable/selector_contact_button"
android:drawableLeft="@android:drawable/ic_menu_edit"
android:onClick="backupsms"
/>
<ProgressBar
android:id="@+id/pb_atools_sms"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="还原短信"
android:background="@drawable/selector_contact_button"
android:drawableLeft="@android:drawable/ic_menu_edit"
android:onClick="restoresms"
/>
</LinearLayout>
上边给短信备份按钮设置的点击事件是backupsms,还原短信按钮设置的点击事件是restoresms
2.到AToolsActivity中实现这两个点击事件的方法
AToolsActivity.java
public class AToolsActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_atool);
}
public void address(View v){
//跳转到查询号码归属地界面
Intent intent = new Intent(this,AddressActivity.class);
startActivity(intent);
}
/**
-
短信备份
-
@param v
*/
public void backupsms(View v){} /**
-
还原短信
-
@param v
*/
public void restoresms(View v){} }
先来实现短信备份:
首先要获取短信,获取短信的操作属于业务类操作,在engine包下创建一个SmsEngine,并创建一个获取全部短信的方法getAllSMS
通过DDMS给模拟器发送几条短信,Incoming number写成1888888888,Message写成i miss you,模拟器5554收到了短信
给它回复一条i miss you too
发送和接收的短信在手机系统哪里保存?
打开data/data/com.android.providers.telephony/dataases文件夹,它里边有个mmssms.db 把它导出来,打开sms表,body保存着i miss you,i miss you too address保存着发件人/收件人188888888,1866666666 data字段,保存的是发送/接收时间 type,1代表接收,2代表发送,
要想获取短信,只需要打开手机系统的数据库mmssms.db查询下sms表就可以
mmssms.db数据库和手机联系人contacts数据库一样,不可写,所以又要用到内容解析者
获取短信是从手机系统中获取,只要是获取系统资源,一般都要用到内容提供者,这个在获取手机联系人时讲过
3.使用内容提供者来获取短信
1)获取内容解析者ContentResolver
需要context去获取到,在getAllSMS方法的参数处需要将Context传递进来
2)拿到内容解析者resolver之后,需要获取内容提供者地址
内容提供者地址怎么获取,原先获取联系人时,去查了下源码里边的清单文件获取短信也一样
打开Day01资料/pacakges/providers/TelephonyProvider/AndroidMinifest.xml 它里边有一个短信的内容提供者,即: <provider android:name="SmsProvider" android:authorities="sms" android:multiprocess="true" android:readPermission="android.permission.READ_SMS" android:writePermission="android.permission.WRITE_SMS"/> 看到“短信的内容提供者”路径authorities是sms
还要获取“sms表”的地址
这个找起来也很简单,查看源码TelephonyProvider/src/com/android/provider/telephony/SmsProvider.java 它里边首先查询下UriMatcher,它是查询地址的,找到如下: sURLMartcher.addURI("sms",null,SMS_ALL);
看到SMS_ALL是null,就是说不需要sms表的地址,直接使用“短信的内容提供者”路径“sms”就可以
3)生成查询的地址,来个Uri.parse(uriString),参数uriString直接写成"content://sms"就可以
4)查询数据
用内容解析者resolver调用query(uri,projection,selection,selectionArgs,sortOrder)方法
参数uri:上边生成的查询地址 参数projection:查询的字段 这里要查询哪些字段? 是谁得知道,那address字段肯定要查询,时间data得知道,type(发件/收件)也得知道,body(短信内容)也得知道 那需要查询的字段有:address,data,type,body 那参数projection就可以写成: new String[]{"address","data","type","body"} 参数selection:查询条件,不需要,写成null 参数selectionArgs:查询参数,不需要,写成null 参数sortOrder:排序,不需要,写成null
接下来通过while循环去解析cursor,通过cursor.getString(columnIndex)获取查询出来的字段
参数columnIndex写成0,会返回一个String类型的address 参数columnIndex写成1,会返回一个String类型的date 参数columnIndex写成2,会返回一个String类型的type 参数columnIndex写成3,会返回一个String类型的body
注意,获取短信属于侵犯用户隐私,需要添加读取短信权限:
android.permission.READ_SMS
还有一个还原短信,现在就给它添加写入短信的权限,即:
android.permission.WRITE_SMS
输出查询出来的字段看下
public class SmsEngine { /**
-
获取短信
*/
public static void getAllSMS(Context context){//1.获取内容解析者 ContentResolver resolver = context.getContentResolver(); //2.获取内容提供者地址sms sms表的地址 //3.生成查询的地址 Uri uri = Uri.parse("content://sms"); //4.查询数据 Cursor cursor = resolver.query(uri, new String[]{"address","date","type","body"}, null, null, null); while(cursor.moveToNext()){ String address = cursor.getString(0); String date = cursor.getString(1); String type = cursor.getString(2); String body = cursor.getString(3); System.out.println("address:"+address+" date:"+date+" type:"+type+" body:"+body); } } }
获取短信的功能已经实现,业务操作都需要进行单元测试,在TestMobilesafexian02项目中,创建下单元测试类TestSms,继承自AndroidTestCase,然后创建一个testSMS方法,方法中就调用mobilesafexian02项目中的SmsEngine中的getAllSMS方法去进行测试
public class TestSms extends AndriodTestCase{ public void testSMS(){ SmsEngine.getAllSMS(getContext()); } }
选中testSMS方法右键run as中选择Android JUnit Test
在logcat中,看到有两个已经获取出来了:
system.out address:1 888-888-8888 date:1440297616124 type: 2 body:i miss you system.out address:18888888888 date:1440297603276 type:1 body: i miss you
到这,获取系统短信的操作就做完了
11.6 保存短信 # (重点)
获取短信操作实现了,需要将获取到的短信保存到数据库或文件中,数据库操作已经做过很多,这里改用保存到文件当中
一般的都是保存到一个xml文件中
先来写个xml文件:
xml文件要有一个文件头,打开清单文件AndroidManifest.xml,看到它就有一个文件头,即:
<?xml version="1.0" encoding="utf-8">
把上边这个文件头拷贝到新建的xml文件中
紧接着来个跟标签smss,跟标签写完后就可以写每一条短信了,短信有多条,用sms标签表示每一条短信
在sms标签中,可以写每个数据表示的标签,比如address,date,type
这是一条短信,其他短信也可以用sms标签分别来表示啊
<?xml version="1.0" encoding="utf-8">
<smss>
<sms>
<address>123456</address>
<date>235456675333</date>
<type>1<type>
<body>i miss you </body>
</sms>
<sms>
<address>112434535</address>
<date>232575333</date>
<type>2<type>
<body>i miss you too </body>
</sms>
</smss>
在SmsEngine中的getAllSMS方法中,按照上边写的xml文件用代码生成这样一个xml文件
用Xml.newSerializer(),返回一个XmlSerializer,是一个接口类型,把它叫做serializer
获取到xml序列器serializer之后,接下来需要创建xml,创建xml文件需要知道文件头,还需要知道这个xml保存在哪
用serializer.setOutput(os,encoding)设置“文件保存位置及保存的编码格式”
参数os:outputString 保存位置,我们指定这个xml保存到"/mnt/sdcard/backup.xml"
参数encoding:编码格式,指定为"utf-8"
它会报异常,try catch捕获一下
用serializer.startDocument(encoding,standalone)设置文件的头信息
参数encoding:编码格式,这个文件头的编码格式是utf-8
参数standalone:是否以单独形式保存,来个true
有startDocument还应该有endDocument
endDocument之后,调用flush方法刷新数据到文件中
用serializer.startTag(namepace,name)设置根标签
参数namespace:命名空间,我们有没有,那设置成null
参数name: 跟标签名称,就是我们的smss
有startTag还应该有endTag(namespace,name),同样的操作,namespace设置成null,name设置成smss
设置每条短信的标签时,因为有多条短信,所以在while循环中来获取每条短信的标签sms
还是用serializer.startTag(namespace,name),serializer.endTag(namespace,name)
但name是每条短信的标签sms
将每条数据的标签设置完之后,就要给对应的标签中保存数据了
上节课已经通过cursor.getString(),参数分别设置成0,1,2,3,从手机短信的数据库mmssms.db中
分别获取出来了address,date,type,body数据
用serializer.text(text)方法,将数据address,date,type,body分别放入这个参数处,就给对应的标签中保存了数据
public class SmsEngine {
/**
-
获取短信
*/
public static void getAllSMS(Context context){//1.获取内容解析者 ContentResolver resolver = context.getContentResolver(); //2.获取内容提供者地址sms sms表的地址 //3.生成查询的地址 Uri uri = Uri.parse("content://sms"); //4.查询数据 Cursor cursor = resolver.query(uri, new String[]{"address","date","type","body"}, null, null, null); //1.获取xml序列器 XmlSerializer serializer = Xml.newSerializer(); try { //2.设置文件保存位置及保存的编码格式 //os:流信息 //encoding:编码格式 serializer.setOutput(new FileOutputStream(new File("/mnt/sdcard/backup.xml")), "utf-8"); //3.设置文件头信息 //standalone : 是否已单独文件保存 serializer.startDocument("utf-8", true); //4.根标签 serializer.startTag(null,"smss"); while(cursor.moveToNext()){ //5.设置每条短信的标签 serializer.startTag(null,"sms"); serializer.startTag(null, "address"); String address = cursor.getString(0); //6.将数据保存到对应标签中 serializer.text(address); serializer.endTag(null, "address"); serializer.startTag(null, "date"); String date = cursor.getString(1); serializer.text(date); serializer.endTag(null, "date"); serializer.startTag(null, "type"); String type = cursor.getString(2); serializer.text(type); serializer.endTag(null, "type"); serializer.startTag(null, "body"); String body = cursor.getString(3); serializer.text(body); serializer.endTag(null, "body"); System.out.println("address:"+address+" date:"+date+" type:"+type+" body:"+body); serializer.endTag(null,"sms"); } serializer.endTag(null,"smss"); serializer.endDocument(); //7.刷新数据到文件中 serializer.flush(); }catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
在TestSms.java中选中testSms方法测试一下:
public class TestSms extends AndroidTestCase{ public void testSMS(){ SmsEngine.getALLSMS(getContext()); } }
弹出绿色的进度条,表示运行成功了,来到DDMS中的data/mnt/sdcard,下边有个backup.xml文件,把它导出来:
<smss> <sms> <address>1888888888</address> <date14402976412></date> <type>2</type> <body>I miss you too</boby> </sms> <sms> <dresss>18888888888</address> <date>144434345534</date> <type>1</type> <body>i miss you</body> </sms> </smss>
这就说明短信数据已经成功保存到了xml文件中了
11.7 回调方法 (进度条两种方式) # (重点)
到AToolsActivity中的短信备份方法backupsms()中,调用SmsEngine中的getAllSMS()方法
运行程序,点击高级工具中的短信备份按钮,在logCat中打印出获取出来的短信内容,说明保存成功
用户点击“短信备份”按钮之后,一般都会来个进度条,提示用户当前进度xxx/总进度xxx
以前进度条一直用的是ProgressBar,这里使用一种新的进度条ProgressDialog
在AToolsActivity的backupsms()方法中new一个ProgressDialog
progressDialog.setCancelable(flag),设置可否取消,false不可取消
这个操作在dialog中有,那这个progressDialog也有这个功能,因为progressDialog有部分dialog的操作
progressDialog.setMax(max),设置总进度,max就是总进度
progressDialog.setProgress(value),设置当前进度,value就是当前进度
progressDialog.show(),展示
这些都是它常用的一些方法
但是在AToolsActivity中不知道备份短信的当前进度xxx/总进度xxx是多少
而SmsEngine的getAllSMS()方法知道前进度xxx/总进度xxx是多少
所以在AToolsActivity的backupsms()方法中调用SmsEngine的getAllSMS()方法时,参数处给SmsEngine的getAllSMS()方法传一个progressDialog过去
public class AToolsActivity extends Activity {
private ProgressDialog progressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_atool);
}
public void address(View v){
//跳转到查询号码归属地界面
Intent intent = new Intent(this,AddressActivity.class);
startActivity(intent);
}
/**
-
短信备份
-
@param v
*/
public void backupsms(View v){progressDialog = new ProgressDialog(getApplicationContext()); //设置不能取消 progressDialog.setCancelable(false); //progressDialog.setMax(max);//设置总进度,max:总进度 //progressDialog.setProgress(value);//设置当前进度,value:当前进度 progressDialog.show(); SmsEngine.getAllSMS(getApplicationContext(),progressDialog); } /**
-
还原短信
-
@param v
*/
public void restoresms(View v){} } 在SmsEngine.java中的getAllSMS()方法的参数中,接收这个ProgressDialog,这样在getAllSMS方法中就可以直接用这个progressdialog了 设置总进度setMax(max),这个总进度max是多少 查询出来几条短信就是几条,cursor中有个getCount(),它会返回一个int类型的count,它就是获取查询出来的数据条目 那这个count就是总进度max 设置当前进度,while循环一次就要拿到一个,来个int progress = 0,这个代表 当前的进度 然后在while循环里边progress++,让它累加一下,然后用pd去setProgress(value); 将这个当前进度progress放到value参数处 /**
-
获取短信
*/
public static void getAllSMS(Context context,ProgressDialog pd){//1.获取内容解析者 ContentResolver resolver = context.getContentResolver(); //2.获取内容提供者地址sms sms表的地址 //3.生成查询的地址 Uri uri = Uri.parse("content://sms"); //4.查询数据 Cursor cursor = resolver.query(uri, new String[]{"address","date","type","body"}, null, null, null); //获取查询出来的数据条目 int count = cursor.getCount(); //设置总进度 pd.setMax(count); //1.xml序列器 XmlSerializer serializer = Xml.newSerializer(); try { //2.文件保存位置及保存的编码格式 //os : 流信息 //encoding : 编码格式 serializer.setOutput(new FileOutputStream(new File("/mnt/sdcard/backup.xml")), "utf-8"); //3.设置文件头信息 //standalone : 是否已单独文件保存 serializer.startDocument("utf-8", true); //4.根标签 serializer.startTag(null, "smss"); int progress = 0; //当前的进度 while(cursor.moveToNext()){ //5.设置每个短信的标签 serializer.startTag(null, "sms"); //6.设置每个数据的标签,成对出现 serializer.startTag(null, "address"); String address = cursor.getString(0); //7.设置标签的文本内容 serializer.text(address); serializer.endTag(null, "address"); serializer.startTag(null, "date"); String date = cursor.getString(1); serializer.text(date); serializer.endTag(null, "date"); serializer.startTag(null, "type"); String type = cursor.getString(2); serializer.text(type); serializer.endTag(null, "type"); serializer.startTag(null, "body"); String body = cursor.getString(3); serializer.text(body); serializer.endTag(null, "body"); System.out.println("address:"+address+" date:"+date+" type:"+type+" body:"+body); serializer.endTag(null, "sms"); progress++;//累加 pd.setProgress(progress); //设置当前进度 } serializer.endTag(null, "smss"); serializer.endDocument(); //8.将数据刷新到的文件中 serializer.flush(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
运行程序,点击高级工具,点击短信备份,发现奔溃了,查看logcat
原因: java.lang.lllegalStateException:Could not execute method of the activity Caused by: android.view.WindowManager&BadTokenException:Unable to add window --
还记得这个错误在哪里出现过吗?在dialog的使用中,用的是this
在AToolsActivity中的backupsms方法中new ProgressDialog时,上下文用的是getApplicationContext,那得改成this
看下ProgressDialog它继承自AlertDialog,所以AlertDialog要知道你挂载在哪个界面上,所以ProgressDialog也和以前使用dialog时一样,凡是和dialog相关的操作,都要用this上下文来指定挂载在哪个界面上
运行程序, 再点高级工具中的短信备份,出来一个进度条显示的是一个圆圈,我们想显示的是一个进度条
progressDialog中有个setProgressStyle(style),将参数style设置成ProgressDialog.STYLE_HORIZONTAL
这是将它的样式设置为水平的进度条
public class AToolsActivity extends Activity { private ProgressDialog progressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_atool); } public void address(View v){ //跳转到查询号码归属地界面 Intent intent = new Intent(this,AddressActivity.class); startActivity(intent); } /**
-
短信备份
-
@param v
*/
public void backupsms(View v){progressDialog = new ProgressDialog(this); //凡是和dialog相关的都要用成this,来指定挂载在哪个界面上 //设置不能取消 progressDialog.setCancelable(false); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设置进度条样式为水平 //progressDialog.setMax(max);//设置总进度,max:总进度 //progressDialog.setProgress(value);//设置当前进度,value:当前进度 progressDialog.show(); SmsEngine.getAllSMS(getApplicationContext(),progressDialog); } /**
-
还原短信
-
@param v
*/
public void restoresms(View v){} }
运行程序,点击高级工具中的短信备份,这时候显示的就是水平的进度条了
但太快了,这个很好解决,在SmsEngine.java中的getAllSMS方法中的while循环中,可以让它睡几秒钟
即:SystemClock.sleep(1000);//当前进度显示太快了,让它每获取一次短信都睡1秒即可
在这里让它睡了1秒,但是在AToolsActivity.java中的backupsms方法中调用的SmsEngine.java中的getAllSMS方法是在主线里边
应该将调用getAllSMS方法的操作,放到子线程当中去执行了
当调用getAllSMS方法查询完之后,还要让这个dialog隐藏掉,在这里用progressDialog去调用dismiss()方法
这里直接在子线程中调用了progressDialog.dismiss()去让这个dialog消失
这是在子线程中执行了更改UI的操作,以前都要new一个Handler,在主线程中去执行更改UI的操作 但是注意:凡是和progress相关的操作,都可以在子线程中去执行 progressDialog可以在子线程中进行UI更新,这个大家一定要注意 public class AToolsActivity extends Activity { private ProgressDialog progressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_atool); } public void address(View v){ //跳转到查询号码归属地界面 Intent intent = new Intent(this,AddressActivity.class); startActivity(intent); } /**
-
短信备份
-
@param v
*/
public void backupsms(View v){progressDialog = new ProgressDialog(this); //凡是和dialog相关的都要用成this,来指定挂载在哪个界面上 //设置不能取消 progressDialog.setCancelable(false); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设置进度条样式为水平 //progressDialog.setMax(max);//设置总进度,max:总进度 //progressDialog.setProgress(value);//设置当前进度,value:当前进度 progressDialog.show(); new Thread(){ public void run() { SmsEngine.getAllSMS(getApplicationContext(),progressDialog); progressDialog.dismiss(); }; }.start(); } /**
-
还原短信
-
@param v
*/
public void restoresms(View v){} }
progressDialog已经实现了,还有一个progressBar也可以用
给大家实现下progressBar
首先在activity_atool.xml中给它增加一个ProgressBar:
<ProgressBar android:id="@+id/pb_atools_sms" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" />
到AToolsActivity中的onCreate方法中初始化这个pb_atools_sms
刚才用的是在backupsms方法中new出来的ProgressDialog,以及它的一系列set操作,现在把他们都注释掉
改用布局文件中的ProgressBar在onCreate方法中初始化出来的pb_atools_sms
在原来调用getAllSMS方法时,参2传递的是progressDialog,现在改成pb_atools_sms,这样就将ProgressBar传递过去了
public class AToolsActivity extends Activity { private ProgressDialog progressDialog; private ProgressBar pb_atools_sms; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_atool); pb_atools_sms = (ProgressBar) findViewById(R.id.pb_atools_sms); } public void address(View v){ //跳转到查询号码归属地界面 Intent intent = new Intent(this,AddressActivity.class); startActivity(intent); } /**
-
短信备份
-
@param v
*/
public void backupsms(View v){//progressDialog = new ProgressDialog(this); //凡是和dialog相关的都要用成this,来指定挂载在哪个界面上 //设置不能取消 //progressDialog.setCancelable(false); //progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设置进度条样式为水平 //progressDialog.setMax(max);//设置总进度,max:总进度 //progressDialog.setProgress(value);//设置当前进度,value:当前进度 //progressDialog.show(); new Thread(){ public void run() { SmsEngine.getAllSMS(getApplicationContext(),pb_atools_sms); //progressDialog.dismiss(); }; }.start(); } /**
-
还原短信
-
@param v
*/
public void restoresms(View v){} }
在SmsEngine的getAllSMS方法的第二个参数处,将原来的ProgressDialog改为ProgressBar,这样就接收了ProgressBar
看下思路,在布局文件中添加progressbar控件,在onCreate方法中初始化,然后将它传递给getAllSMS方法并接收
运行程序,点击高级工具中的短信备份,看到这个Progressbar(一条线状的进度条)也可以,这个是第二种方式
进度条的两种实现方式总结如下:
1.progressdialog(默认为圆形,setProgressStyle可将进度条设置为长方形)
progressDialog = new ProgressDialog(this); //设置不能取消 progressDialog.setCancelable(false); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
// progressDialog.setMax(max);//设置总进度,max:总进度
// progressDialog.setProgress(value);//设置当前进度,value :当前进度
progressDialog.show();
2.progerssbar(线状的进度条),操作跟progressDialog相似
都有setMax(设置总进度),setProgress(设置当前进度)等操作,在上边改用Progressbar时,给getAllSMS方法传递ProgressBar时,是不是只改了接收ProgressBar,下边的操作都没有修改,因为它们是一样的操作,都可以在子线程中去更新
接下来要给大家说的是另外一件事情
老板说来新需求了,觉得progressbar这个比较难看,把两个都给显示出来比较下看哪个比较好看来取舍下
这时我们要改,要在调用getAllSMS方法时,增加第三个参数为ProgressDialog,并将backupsms方法中注释掉的ProgressDialog的相关操作都放开,然后到SmsEngine的getAllSMS方法的参数处增加第三个参数用来接收ProgressDialog,并给接收的ProgressBar起名叫pb,ProgressDialog起名叫pd
因为ProgressBar和ProgressDialog操作是不是都一样,那接下来在getAllSMS中的具体设置操作,直接复制一份,改为pb
运行程序,点击高级工具中的短信备份,两个进度条都有了
老板一比较还是对话框好看,你再改回来,那我们要把调用getAllSMS方法时传递的第二个参数pb_atools_sms去掉
然后要来到业务类SmsEngine的getAllSMS方法中,去掉接收的第二个参数ProgressBar,将关于ProgressBar的操作都注释掉
然后运行程序,点击高级工具中的短信备份,就只剩下ProgressDialog了
只要老板的想法一直在变,你就要一直去改业务类SmsEngine中的getAllSMS方法
在开发中你们都是分模块开发的,所以都是自己干自己的,这个业务类SmsEngine.java可能是别人写的,你让别人改的次数多了会不乐意
所以前期就要考虑好,在这里一直要频繁的去修改业务类SmsEngine
用一个回调方法来解决频繁去修改业务类的问题
在业务类SmsEngine的成员变量处,写个接口,把它叫做BackUpSMSListener
然后在接口中来两个方法,一个是叫做max,它需要一个int类型的max
一个叫做progress,它需要一个int类型的progress
接口写好之后,把这个接口以参数的形式,在getAllSMS方法处传递给调用它的地方
在getAllSMS方法中
设置总进度时,不用具体的pd.setMax(count),或pb.setMax(count)了
而是用listener.max(count),将count值传递给调用getAllSMS方法的地方
设置当前进度时,也不用具体的pd.setProgress(progress),或pb.setMax(count)了
而是用listener.progress(progress),将progress值传递给调用getAllSMS方法的地方
public class SmsEngine {
public interface BackUpSMSListener{
/**
-
设置总进度
-
@param max
/
public void max(int max);
/* -
设置当前进度
-
@param progress
*/
void progress(int progress);
}/**
-
获取短信
*/
public static void getAllSMS(Context context,BackUpSMSListener listener){//1.获取内容解析者 ContentResolver resolver = context.getContentResolver(); //2.获取内容提供者地址sms sms表的地址 //3.生成查询的地址 Uri uri = Uri.parse("content://sms"); //4.查询数据 Cursor cursor = resolver.query(uri, new String[]{"address","date","type","body"}, null, null, null); //获取查询出来的数据条目 int count = cursor.getCount(); //设置总进度 // pd.setMax(count); // pb.setMax(count); listener.max(count); //1.xml序列器 XmlSerializer serializer = Xml.newSerializer(); try { //2.文件保存位置及保存的编码格式 //os : 流信息 //encoding : 编码格式 serializer.setOutput(new FileOutputStream(new File("/mnt/sdcard/backup.xml")), "utf-8"); //3.设置文件头信息 //standalone : 是否已单独文件保存 serializer.startDocument("utf-8", true); //4.根标签 serializer.startTag(null, "smss"); int progress = 0;//当前进度 while(cursor.moveToNext()){ SystemClock.sleep(1000); //5.设置每个短信的标签 serializer.startTag(null, "sms"); //6.设置每个数据的标签,成对出现 serializer.startTag(null, "address"); String address = cursor.getString(0); //7.设置标签的文本内容 serializer.text(address); serializer.endTag(null, "address"); serializer.startTag(null, "date"); String date = cursor.getString(1); serializer.text(date); serializer.endTag(null, "date"); serializer.startTag(null, "type"); String type = cursor.getString(2); serializer.text(type); serializer.endTag(null, "type"); serializer.startTag(null, "body"); String body = cursor.getString(3); serializer.text(body); serializer.endTag(null, "body"); System.out.println("address:"+address+" date:"+date+" type:"+type+" body:"+body); serializer.endTag(null, "sms"); progress++; // pd.setProgress(progress);//设置当前进度 // pb.setProgress(progress);//设置当前进度 listener.progress(progress); } serializer.endTag(null, "smss"); serializer.endDocument(); //8.将数据刷新到的文件中 serializer.flush(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
到调用getAllSMS方法的AToolsActivity的backupsms方法中,因为调用的getAllSMS方法的参数改为了接口
所以这里也需要去掉具体的ProgressDialog,ProgressBar,改为接口BackUpSMSListener
然后实现了它的两个方法progress,max,在max方法中用具体的进度条去setMax,在progress方法中用具体的进度条去setProgress
public class AToolsActivity extends Activity { private ProgressDialog progressDialog; private ProgressBar pb_atools_sms; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_atool); pb_atools_sms = (ProgressBar) findViewById(R.id.pb_atools_sms); } public void address(View v){ //跳转到查询号码归属地界面 Intent intent = new Intent(this,AddressActivity.class); startActivity(intent); } /**
-
短信备份
-
@param v
*/
public void backupsms(View v){//progressDialog = new ProgressDialog(this); //凡是和dialog相关的都要用成this,来指定挂载在哪个界面上 //设置不能取消 //progressDialog.setCancelable(false); //progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设置进度条样式为水平 //progressDialog.setMax(max);//设置总进度,max:总进度 //progressDialog.setProgress(value);//设置当前进度,value:当前进度 //progressDialog.show(); new Thread(){ public void run() { SmsEngine.getAllSMS(getApplicationContext(),new BackUpSMSListener() { @Override public void progress(int progress) { // progressDialog.setProgress(progress); pb_atools_sms.setProgress(progress); } @Override public void max(int max) { // progressDialog.setMax(max); pb_atools_sms.setMax(max); } }); //progressDialog.dismiss(); }; }.start(); } /**
-
还原短信
-
@param v
*/
public void restoresms(View v){} }
运行程序,点击高级工具中的短信备份,展示的是ProgressDialog
那老板换需求了,说改用Progressbar,那只需要注释掉AToolsActivity中backupsms方法中关于ProgressDialog的操作
在progress方法中改用pb_atools_sms.setProgress(progress),在max方法中改用pb_atools_sms.setMax(max)即可
整理思路:
首先在AToolsActivity的backupsms方法中调用getAllSMS方法时,它里边有个BackUpSMSListener接口的内部类要去实现 那在SmsEngine中的getAllSMS方法中,是用接口中的两个方法去分别把count,progress传递给调用它的地方 在调用getAllSMS方法时,要去new一个BackUpSMSListener并实现它的监听操作,那它的监听操作中做什么,就在实现progress方法,max方法时实现什么操作
相当于将原来在SmsEngine中的操作,提取到AToolsActivity中去执行,这样就不用去改业务类SmsEngine了
这个操作在开发中也会经常用到,就是用这个回调方法来实现
那下边总结下怎样创建这个回调方法:
创建回调方法步骤 #(重点)
1.在处理的业务类(SmsEngine.java)中,编写一个接口,同时声明出要使用的方法
public interface BackUpSMSListener{
/**
-
设置总进度
-
@param max
/
void max(int max);
/* -
设置当前进度
-
@param progress
*/
void progress(int progress);
}2.将接口以参数的形式,传递给要调用的业务方法,并在其中执行相关的接口的方法
public static void getAllSMS(Context context,BackUpSMSListener listener){ listener.max(count); }
3.在调用业务方法的时候(AToolsActivity中),在方法中实现接口操作,在实现方法中去实现相应的操作
SmsEngine.getAllSMS(getApplicationContext(),new BackUpSMSListener() { @Override public void progress(int progress) { //progressDialog.setProgress(progress); pb_atools_sms.setProgress(progress); } @Override public void max(int max) { //progressDialog.setMax(max); pb_atools_sms.setMax(max); } });
这个就是回调方法的使用步骤
11.8还原短信#
还原短信的操作,其实就是解析xml
这个解析xml没有什么好讲的,用XmlPullParser,基础班时学过xml的解析可以套用
其实根本就不用去写,原因是像这种xml,工作中用到的几率特别小,我工作到现在就用过一次,用的都是json,保存数据用的是数据库
如果去银行工作,对信息进行保密处理,这个时候就要用到xml
这里重点讲解析完数据之后,怎么把数据添加到手机中
前边在SmsEngine中的getAllSMS方法中获取短信来保存时用内容解析者去获取数据,在还原短信时还是用内容解析者
在AToolsActivity的还原短信方法restoresms中写一个ConentResolver
用resolver调用insert(url,values),这个操作和数据库中的操作一模一样,都是有个insert
参数url:需要个url,可以把SmsEngine中getAllSMS方法中的uri拷贝过来
参数values:需要个values,它的类型是ContentValues
安卓中凡是和数据库相关,保存数据或填充数据时,都是用ContentValues
需要一个ContentValues,那在restoresms方法中直接new一个ContentValues,叫做values
用values.put(key,values),分别put短信信息address,date,type,body
date是时间,在数据库中保存的时间,形式是“1440298049000”
所以还原短信时,要用System.currentTimeMillis()+""简单的获取下现在的时间
/**
-
还原短信
-
@param v
*/
public void restoresms(View v){//内容解析者 ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://sms"); ContentValues values = new ContentValues(); values.put("address", 95588); values.put("date", System.currentTimeMillis()+""); values.put("type", 1); values.put("body", "zhuan zhang le $10000000000000000"); resolver.insert(uri, values); }
到这,给手机添加一条短信的操作就做完了
运行程序,点击高级工具中的还原短信,在模拟器的短信中有个95588发送过来的短信,即:
zhuan zhang le $10000000000000000
这就是还原短信,我只写了解析完数据之后该怎么把数据添加到手机中,至于解析xml你们可以自己写写
工作中用到了,百度下一大堆,根本不用去写
像这种生成,解析xml的操作,在工作中都不会刻意去写,一般都是去找或用一些第三方的东西去生成
一般会有比较好的一些第三方让你去用,或直接写一个工具类去用,这都可以
还原短信的操作,主要就是用内容解析者ContentResolver去操作的
11.9可扩展的listview #
打开金山手机卫士,它里边有个通话管理,通话管理里边有个常用号码查询,点开之后
从表面看,它其实就是一个listView列表,只不过在列表里边点击条目时,又会弹出一些子列表,它就是一个可扩展的listview
写一个示例工程“可扩展的listview”
1.在布局文件中布局
<ExpandableListView
android:id="@+id/expandablelistview"
android:layout_width="match_parent"
android:layout_height="match_parent"
2.在代码中调用
expandablelistview = (ExpandableListView) findViewById(R.id.expandablelistview);
expandablelistview.setAdapter(new Myadapter());
adapter:
private class Myadapter extends BaseExpandableListAdapter {
// 获取组的个数
@Override
public int getGroupCount() {
// TODO Auto-generated method stub
return 8;
}
// 获取每组有多少个孩子 groupPosition : 代表的组的位置,从0开始的
@Override
public int getChildrenCount(int groupPosition) {
// TODO Auto-generated method stub
return groupPosition+1;
}
// 获取组的样式
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
TextView textView = new TextView(getApplicationContext());
textView.setText(" 我是第"+groupPosition+"组");
return textView;
}
// 获取组的孩子的样式
//childPosition : 孩子的位置
@Override
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
TextView textView = new TextView(getApplicationContext());
textView.setText("我是第"+groupPosition+"组的第"+childPosition+"个孩子");
textView.setTextColor(Color.RED);
return textView;
}
// 获取组的对象对应的数据
@Override
public Object getGroup(int groupPosition) {
// TODO Auto-generated method stub
return null;
}
// 获取组的孩子的对象的数据
@Override
public Object getChild(int groupPosition, int childPosition) {
// TODO Auto-generated method stub
return null;
}
// 获取组的id
@Override
public long getGroupId(int groupPosition) {
// TODO Auto-generated method stub
return 0;
}
// 获取组的孩子的id
@Override
public long getChildId(int groupPosition, int childPosition) {
// TODO Auto-generated method stub
return 0;
}
// 判断id是否稳定,判断有没有id ,有的true,没有的false
@Override
public boolean hasStableIds() {
// TODO Auto-generated method stub
return false;
}
// 设置孩子是否可以点击,false:可以
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
// TODO Auto-generated method stub
return false;
}
}
主要看的是adapter中实现的四个方法:
1.getGroupCount:获取组的个数
2.getChildrenCount:获取每组有多少个孩子
3.getGroupView:获取组的样式
4.getChildView:获取组的孩子的样式
11.10抽屉的效果# (重点)
剩下流量统计,手机杀毒,缓存清理这三个模块了,这些就比较简单了
先来做流量统计
在HomeActivity中给Gridview设置setOnItemClickListener里边的onItemClick方法中的switch中添加一个流量统计,即:
case 4://流量统计
Intent intent4 = new Intent(HomeActivity.this,TrafficActivity.class);
startActivity(intent4);
break;
需要一个TrafficActivity,创建出来,紧接着到清单文件中去配置一下
然后回到TrafficActivity中重写它的onCreate方法,在onCreate方法中用setContentView加载布局文件activity_traffic.xml
需要一个activity_traffic.xml,创建出来,即:
activity_traffic.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="流量统计"
android:textSize="25sp"
android:gravity="center_horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="#8866ff00"
/>
</RelativeLayout>
<LinearLayout
android:layout_gravity="right"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
</android.support.v4.widget.DrawerLayout> 简单说下流量统计是怎么做的,在android中有一个TrafficStats函数,在这个函数中有一些方法 即: TrafficStats.getMobileRxBytes();//获取手机接收的流量 TrafficStats.getMobileTxBytes();//获取手机上传的流量 TrafficStats.getTotalRxBytes();//获取手机接收的总流量 TrafficStats.getTotalTxBytes();//获取手机上传的总的流量 TrafficStats.getUidRxBytes(uid);//获取指定应用接收的流量 uid,进程的uid TrafficStats.getUidTxBytes(uid);//获取指定应用上传的流量 这个uid怎么获取的呢? 在TaskEngine中的getTaskInfos方法中,通过am去获取到一个pid,在runningAppProcessInfo中还有一个uid 大家都知道,一个应用就相当于一个用户组,这个uid就是你这个用户的id,它也可以唯一的标识一个程序,即: runningAppProcessInfo.uid //获取应用的uid
但是TrafficStats这些方法,在android手机上有一个缺陷,它们去获取数据时,都是从DDMS下的proc文件夹下去获取的
proc有这样一个问题,关于流量方面的操作,你重启手机或长时间不开机,这个流量会归零
所以这种方式不是特别准确
像手机卫士中的流量,它是用了其他方式,第三方的东西去实现,每次它都要发送一个短信来校正流量
这个就是通过和运行商合作,会返回一些流量操作来供手机卫士获取
上边说的TrafficStats这种方式知道就行,在开发中一般用不到它
在流量统计模块主要说下抽屉效果
抽屉效果有两种实现方式
在activity_traffic.xml中实现效果
1.SlidingDrawer
handle:我们在拖动抽屉时,都有这样一个条目,这个条目就是抽屉的把手,有把手就有内容
content:抽屉的内容
handle,content属性,要的都是一个id
下边布局表示LinearLayout是抽屉的把手,FrameLayout是抽屉的内容
<!-- handle : 抽屉的把手
content : 抽屉的内容
-->
<SlidingDrawer
android:layout_width="match_parent"
android:layout_height="match_parent"
android:handle="@+id/handle"
android:content="@+id/content"
android:orientation="horizontal"
<LinearLayout
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/shenmabg"
/>
</LinearLayout>
<FrameLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
运行程序,点击流量统计,按住神马图片,就可以上下拉出红色的背景内容部分了,这个就是抽屉的效果
有些同学问能左右拉出这个抽屉不,可以
只需要给SlidingDrawer添加android:orientation="horizontal"属性即可
有些同学说让这个“神马”把手显示在上边而不是下边,这个也是可以的,只不过需要将ImageView用LinearLayout包裹一下了,并给LinearLayout设置android:orientation="vertical"属性即可
注意:
SlidingDrawer控件缺点:只能从下往上,从右往左拉出抽屉,你要从左往右,从上到下拉出抽屉,这个是不可能的
要实现从左往右,或者从上到下拉出抽屉,那就要使用我们第二种方式了
2.DrawerLayout
DrawerLayout是android自带的,在V4包中的widget包中
只需要记住使用V4包下的DrawerLayout控件即可
要想使用第三方V4的东西,需要DrawerLayout的全类名
注意:
使用DrawerLayout时,必须用DrawerLayout包裹整个布局
在DrawerLayout作为根布局包裹时,里边的其他控件是点儿不出属性的,只能自己一个一个写属性,或者拷贝
设置android:layout_gravity="right"即可从右往左拉出
设置android:layout_gravity="left"即可从左往右拉出
给DrawerLayout设置id为dl,就可以到TrafficActivity中去findViewById初始化控件了
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dl"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="流量统计"
android:textSize="25sp"
android:gravity="center_horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="#8866ff00"
/>
</RelativeLayout>
<LinearLayout
android:layout_gravity="right"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
</android.support.v4.widget.DrawerLayout>
代码中调用:
dl = (DrawerLayout) findViewById(R.id.dl);
//打开左边的控件
dl.openDrawer(Gravity.LEFT);
//打开右边的控件
dl.openDrawer(Gravity.RIGHT);
运行程序,从右往左拖出了红色的内容部分,从左往右拖出了蓝色的内容部分
注意:
重要的是dl.openDrawer(Gravity.LEFT);这个方法,它可以控制你是打开左边还是右边的抽屉
注意:
大家的v4包里边可能找不到DrawerLayout.jar,这是因为大家的v4包版本太低
我给大家的讲课资料里边就准备了一个V4包,把它拷贝到你的sdk下的extras/android/support/v4包下覆盖掉原来的V4包,然后重启eclipse之后,每次创建的项目就都是现在的这个V4包了
这两种方式中,第二种方式DrawerLayout是比较流行的一种,而第一种方式SlidingDrawer,它是以前用的比较多,但是现在基本上已经被淘汰了
抽屉效果最常用的是用来做菜单栏,你点击它就跳转到哪个界面去
11.11杀毒发展史#
接下来实现手机杀毒模块,主要给大家简绍各种特效
1.病毒:计算机程序,恶意行为,三类
1.恶搞,炫耀自己的技术,熊猫烧香,损人不利己
2.灰鸽子 : 盗号的,QQ号,游戏的账号
3.肉机:把你的电脑做成傀儡机,他通过程序控制你的电脑
2013年比特币特别火,一个炒到7千多,还有莱特币那会儿一百多块钱,我们就有老师比特币买的时候几百块钱,卖的时候几千块钱
他买了100多个,这个比特币可以挖矿,那时就有人专门做肉机程序,用别人的电脑帮它去挖矿,因为挖矿需要硬件一些东西支持,他就会用肉机程序把你的电脑当成他的电脑,帮他去挖矿,这个对电脑损伤是比较大的,都用比较好的服务器去做
2.杀毒方式
1.黑名单方式:把一些已有病毒特征码保存到数据库,通过匹配数据库中特征进行操作,滞后性,裸机(没有任何防护的电脑,吸引病毒),裸机就是蜜罐(放到网络的主干道上),现在升级成云查杀了:就是把你的电脑当成蜜罐,发现有病毒就上传
2.白名单方式:360兴起的方式,把已经检测过没问题的程序放到数据库中,当程序运行的时候,如果发现是在数据库中就不在去检测杀毒,可以及时的发现解决病毒
11.12杀毒界面# (重点layer-list)
上边讲了杀毒发展史,接下来实现下杀毒功能
到HomeActivity中,在GridView的onItemClick方法的switch中增加如下:
case 5://手机杀毒
Intent intent5 = new Intent(HomeActivity.this,AntivirusActivity.class);
startActivity(intent5);
break;
需要一个AntivirusActivity,新建一个,然后到清单文件中配置,到AntivirusActivity中重写它的onCreate方法,并通过setContentView加载布局activity_antivirus.xml,需要一个activity_antivirus.xml,创建出来
即:
activity_antivirus.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="手机杀毒"
android:textSize="25sp"
android:gravity="center_horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="#8866ff00"
/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
<ImageView
android:id="@+id/iv_antivirus_antivimageview"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_scanner_malware"
android:layout_margin="5dp"
/>
<ImageView
android:id="@+id/iv_antivirus_scanner"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/act_scanning_03"
android:layout_margin="5dp"
/>
<TextView
android:id="@+id/tv_antivirus_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="正在扫描"
android:layout_toRightOf="@+id/iv_antivirus_antivimageview"
android:layout_marginTop="25dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:singleLine="true"
/>
<!-- progress : 设置progerssbar的进度 -->
<ProgressBar
android:id="@+id/pb_antivirus_progressbar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_antivirus_name"
android:layout_toRightOf="@+id/iv_antivirus_antivimageview"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:progressDrawable="@drawable/anti_progressbar_drawable"
android:layout_marginTop="5dp"
/>
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
<LinearLayout
android:id="@+id/ll_antivirus_safeapk"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
这个手机杀毒界面是模仿金山卫士做的:
它有一个类似于雷达的扫描图片,里边有个灰色的东西一直在转,其实是两张图片重叠在一起做的动画,我们用RelativeLayout包裹,RelativeLayout和FrameLayout效果其实一样,最下边的图片显示在最上边
右边有一个长方形进度条,进度条上边是文字“正在扫描xxx”,下边是查杀了哪些文件都用ScrollView展示列举出来了
长方形进度条样式和我们要的不一样,还记得前边ProgressBar样式是怎样去自定义的
自定义ProgressBar样式:
到sdk中去看下,sdk/platforms/android-16(随便挑一个版本)/res/values/styles.xml,它就是控件的样式文件
这个progressBar样式是水平的,那找到一个符合的,即:
<style name="Widget.ProgressBar.Horizontal">
<item name="android:indeterminateOnly"> false </item>
<item name="android:progressDrawable>@android:drawable/progress_horizontal"</item>
<item name="android:indeterminateDrawable">@android:drawable/progress_indeterminate_horizontal</item>
<item name="android:minHeight">20dip</item>
<item name="android:maxHeight">20dip</item>
</style>
indeterminateOnly:控制有没有进度条的
属性progressDrawable:设置ProgressBar图片的,它是一个progress_horizontal.xml文件
progress_horizontal.xml如下:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@android:id/background"
<bitmap android:src="@drawable/security_progress_bg"
android:gravity="center" />
</item>
<item
android:id="@android:id/secondaryProgress"
<bitmap android:src="@drawable/security_progress"
android:gravity="center" />
</item>
<item
android:id="@android:id/progress"
<bitmap android:src="@drawable/security_progress"
android:gravity="center" />
</item>
</layer-list>
layer-list,类似于framlayout和RelativeLayout,可以让控件一个控件叠着另一个控件,最下面的显示的时候是在最上边
看下这个api中它是怎么定义的,像layer-list这种操作,在api中都有说明怎么去做
找到sdk/docs/index.html,develop/APIGuides/App Resources/Resource Types/Drawable,在右边看到有Layer-list,打开看下
它说很简单,在res/drawable/下创建个filename.xml文件
然后它里边可以有item,可以设置drawable,id,top,right,bottom,left等等属性
看下它的example,并拷贝到在drawable文件夹下新建的anti_progressbar_drawable.xml中,即:
anti_progressbar_drawable.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@android:id/background"
<bitmap android:src="@drawable/security_progress_bg"
android:gravity="center" />
</item>
<item
android:id="@android:id/secondaryProgress"
<bitmap android:src="@drawable/security_progress"
android:gravity="center" />
</item>
<item
android:id="@android:id/progress"
<bitmap android:src="@drawable/security_progress"
android:gravity="center" />
</item>
</layer-list>
注意,item的id是不能变的,从Progress_horizontal.xml中拷贝过来,之所以不能变是因为这个ProgressBar在用时,是根据这个id来调用的,我们改变的只是图片,所以id一定不能改
这个图片替换成我们的security_progress_bg(灰色),security_progress
那接下来就可以调用我们在drawable文件夹下写好的这个anti_progressbar_drawable.xml
前边说过,要在布局文件中覆盖掉原来的属性,直接在布局文件中使用这个属性就可以
到activity_antivirus.xml中:
给ProgressBar添加属性:android:progressDrawable="@drawable/anti_progressbar_drawable"
这样就实现了自定义ProgressBar样式
自定义ProgressBar样式步骤如下:
1.在res-> drawable ->新建xxx.xml 注意:id不可以变
anti_progressbar_drawable.xml如下:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@android:id/background"
<bitmap android:src="@drawable/security_progress_bg"
android:gravity="center" />
</item>
<item
android:id="@android:id/secondaryProgress"
<bitmap android:src="@drawable/security_progress"
android:gravity="center" />
</item>
<item
android:id="@android:id/progress"
<bitmap android:src="@drawable/security_progress"
android:gravity="center" />
</item>
</layer-list>
2.使用
在布局文件activity_antivirus.xml的progressBar控件中使用anti_progressbar_drawable.xml即可
<!-- progress : 设置progerssbar的进度 -->
<ProgressBar
android:id="@+id/progressBar1"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_antivirus_name"
android:layout_toRightOf="@+id/iv_antivirus_antivimageview"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:progressDrawable="@drawable/anti_progressbar_drawable"
android:progress="30"
android:layout_marginTop="5dp"
/>
11.13扫描程序# (重点)
手机杀毒模块的界面应该有动画,扫描图片应该一直在旋转,而这个ProgressBar也应该一直在往前走这个进度
在AntivirusActivity中先初始化下控件,把旋转的这个图片初始化出来
然后要让这个imageView旋转,需要来一个旋转动画RotateAnimation
既然是扫描程序杀毒,接下来应该去获取下程序了,那在onCreate方法中调用scanner()方法,然后创建这个方法
然后可以到scanner方法中去获取程序,获取程序需要一个PackageManager,在pm中有getInstalledPackages(flags)方法获取到所有安装的应用程序,flags写成0,意思是获取所有已安装的应用程序
获取所有已安装的应用程序是不是耗时操作,所以要放到一个子线程中
然后可以遍历查询出来的list集合,每遍历扫描出来一个应用程序,进度条都要往前走一下
所以要设置ProgressBar的总进度,这个总进度是集合的长度
先把这个ProgresBar初始化出来,设置总进度前边刚讲过
通过pb_antivirus_progressbar去setMax(installedPackages.size())即可
这个总进度有了之后,还要设置当前进度,先给它个初始值
int progress = 0;
然后for循环一次,累加一下,并将当前进度progress设置给ProgressBar控件pb_antivirus_progressbar
运行程序,发现进度条当前进度太快了,那每次for循环时让它睡上100毫秒增加进度显示的真实性
在查询时也可以让他睡上1000毫秒,让程序延时1秒钟扫描
进度条上边应该显示正在扫描哪个程序,那获取下应用的名称
通过packageInfo中的applicationInfo去调用loadLabel(pm),参数写成pm,然后toString()成字符串
就可以获取到它的名称了,接下来可以把返回到的这个name设置给TextView控件去显示
所以还要初始化下这个textview控件,不能在子线程中直接去setText
因为不能在子线程中直接更新主线程UI,那可以用runOnUiThread()方法,在它的run方法中就可以去给textview去setText了
前边有个1000毫秒的停顿时间,那为了用户体验,也可以在扫描之前给textview设置下默认文字为“正在初始化64核扫描引擎...”
运行程序,没问题,但是发现有些被扫描的应用名称很长,会变为2行
那在activity_antivirus.xml中给textview来个singleLine属性,值为true,这样它就不会换行显示了
即:
<TextView
android:id="@+id/tv_antivirus_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="正在扫描"
android:layout_toRightOf="@+id/iv_antivirus_antivimageview"
android:layout_marginTop="25dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:singleLine="true"
/>
运行程序,长的它后边就直接省略号代替了
但是扫描结束之后,它还在扫描最后一个应用,那扫描结束后应该提示“扫描完成”并且停止旋转动画
在for循环里边执行扫描操作,那在for循环外边也就是扫描结束之后,同样还要来个runOnUiThread,在它的run方法中就可以给textview去setText设置“扫描完成,发现病毒”,同时还要调用clearAnimation把动画给停止
运行程序,扫描结束之后,提示“扫描完成,很安全”,下边还要显示扫描的每个应用的名称
显示扫描的每个应用程序名称的方式:
1.用listview不断的去更改界面,这个很简单前边也做过
2.scrollView,你可以不断的给里边添加textview,这是没问题的
这里选用第2种方式ScrollView
在Activity_Antivirus.xml中添加用LinearLayout包裹的scrollView,然后在AntivirusActivity中初始化控件ll_antivirus_safeapk
每扫描一个就要在下边列出来这个应用的名称
那在“正在扫描:”下边new一个TextView,然后给这个textView去setText,给它传name,然后还可以setTextColor给它设置成黑色black,最后给控件ll_antivirus_safeapk去addView,将textview传递进去
public class AntivirusActivity extends Activity {
private ImageView iv_antivirus_scanner;
private ProgressBar pb_antivirus_progressbar;
private TextView tv_antivirus_name;
private LinearLayout ll_antivirus_safeapk;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_antivirus);
iv_antivirus_scanner = (ImageView) findViewById(R.id.iv_antivirus_scanner);
pb_antivirus_progressbar = (ProgressBar) findViewById(R.id.pb_antivirus_progressbar);
tv_antivirus_name = (TextView) findViewById(R.id.tv_antivirus_name);
ll_antivirus_safeapk = (LinearLayout) findViewById(R.id.ll_antivirus_safeapk);
//旋转动画
//参数1:开始的角度
//参数2:结束的角度
//剩下的参数:控制动画的位置
RotateAnimation rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(2000);
//旋转的次数
rotateAnimation.setRepeatCount(Animation.INFINITE);//INFINITE : 一直旋转
//动画插入器 解决旋转停顿的问题
LinearInterpolator interpolator = new LinearInterpolator();
rotateAnimation.setInterpolator(interpolator);
iv_antivirus_scanner.startAnimation(rotateAnimation);
scanner();
}
/**
-
扫描程序
*/
private void scanner() {
//1.获取包的管理者
final PackageManager pm = getPackageManager();tv_antivirus_name.setText("正在初始化64核扫描引擎..."); new Thread(){ public void run() { //5.睡1秒,让程序延时1秒钟扫描 SystemClock.sleep(1000); //2.获取安装的程序 参数必须设置标签,才可以获取到(在log中)签名信息(特别长) 否则会报空指针异常 List<PackageInfo> installedPackages = pm.getInstalledPackages(PackageManager.GET_SIGNATURES); //3.设置progerssbar的总进度 pb_antivirus_progressbar.setMax(installedPackages.size()); //设置当前进度 int progress = 0; for (final PackageInfo packageInfo : installedPackages) { //5.睡100毫秒,增加进度显示的真实性 SystemClock.sleep(100); //4.设置当前进度 progress++; pb_antivirus_progressbar.setProgress(progress); //6.获取应用的名称 final String name = packageInfo.applicationInfo.loadLabel(pm).toString(); runOnUiThread(new Runnable() { @Override public void run() { //7.设置正在扫描的应用名称 tv_antivirus_name.setText("正在扫描:"+name); //9.设置在下方空白处显示扫描显示的应用成名称 TextView textView = new TextView(getApplicationContext()); textView.setText(name); textView.setTextColor(Color.BLACK); ll_antivirus_safeapk.addView(textView, 0);//index:将view添加到哪个位置 } }); } runOnUiThread(new Runnable() { @Override public void run() { tv_antivirus_name.setText("扫描完成,发现病毒"); //停止动画 iv_antivirus_scanner.clearAnimation(); } }); }; }.start(); } }
运行程序,将每次扫描的应用名称都罗列出来了,但是发现它不能滑动,那用户如果想看扫描的下边的应用程序名称看不到
所以这里就用到了scrollView,布局文件中将scrollview添加给LinearLayout
运行程序,下边的应用程序名称也可以通过滑动查看到了
有些同学说让它每检测一个应用程序,就自动从下往上滑动到显示他的地方,这个就比较难了,你们可以自己去实现下
ScrollView自动从下往上滑动到显示的位置
1.要让它自动滑,需要拿到ScrollView,每一次都去计算它的位置 2.可以让它每次都从上往下添加应用的名称
这里用第2种方式实现下
这个就涉及到重载的方法,将给ll_antivirus_safeapk去addView()时,使用两个参数的,即addView(textview,index)
这个index我写个0,index的意思是将这个view添加到哪个位置,0就表示添加到第0个位置
那它每次添加都是添加到最上边,那每次都从上往下添加应用的名称
这个整体扫描的效果出来了,但是没有扫描,那这个明天去讲
上边写的手机杀毒功能的步骤总结如下:
旋转动画
//旋转动画 //参数1:开始的角度 //参数2:结束的角度 //剩下的参数:控制动画的位置 RotateAnimation rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); rotateAnimation.setDuration(2000); //旋转的次数 rotateAnimation.setRepeatCount(Animation.INFINITE);//INFINITE : 一直旋转 //解决旋转停顿的问题 LinearInterpolator interpolator = new LinearInterpolator(); rotateAnimation.setInterpolator(interpolator); iv_antivirus_scanner.startAnimation(rotateAnimation);
进度条及扫描效果的实现:
见上边扫描程序的方法scanner
14.总结 ##