手机卫士笔记
手机卫士笔记
手机卫士笔记
Day01
项目介绍
演示功能有:
- 启动页面
- 主页
- 手机防盗(注意:演示时模拟器要提前设置有联系人);
- 通讯卫士:黑名单的管理:电话拦截、短信拦截的演示;
- 软件管理:列出系统的所有软件,启动软件、卸载软件、系统的卸载失败(需要root权限这个后面也会介绍)
- 进程管理:列出系统中正在运行的程序;演示杀死软件
- 窗口小部件:添加桌面;
- 流量统计:模拟器并不支持,在真机上才能演示,只做个UI效果;
- 手机杀毒:检查手机安装的软件,发现那个是病毒,提醒用户就杀掉;
- 系统优化:清楚系统的垃圾,刚开始运行,没用多余数据;
- 高级工具:归属地查询;常用号码查询;短信备份;
svn工具使用
为什么要安装svn服务器?
方便学生从老师的电脑随时checkout代码,也方便学生更有效得管理自己的代码
- 安装VisualSVN Server
- VisualSVN Server的使用
- 创建仓库
- 创建用户,针对不同用户设置不同权限
- checkout代码,commit代码
- 从已有的仓库中引入项目
代码组织结构
- 按照业务模块划分
办公软件 -- 开会 com.itheima.meeting -- 发放工资 com.itheima.money -- 出差 com.itheima.travel 网盘 -- 上传 com.sina.vdisk.upload -- 下载 com.sina.vdisk.download -- 文件分享 com.sina.vdisk.share
- 按照组件划分
界面 com.itheima.mobilesafe.activies 自定义UI com.itheima.mobilesafe.ui 业务逻辑代码 com.itheima.mobilesafe.engine 数据引擎业务逻辑 获取解析数据 数据库持久化 com.itheima.mobilesafe.db com.itheima.mobilesafe.db.dao 广播接收者 com.itheima.mobilesafe.receiver 长期在后台运行 com.itheima.mobilesafe.service 公用的api工具类 com.itheima.mobilesafe.utils
Splash页面
- Splash页面作用
- 展示品牌logo
- 程序初始化
- 检查版本更新
- 校验程序合法性,比如某些app会判断用户是否联网, 没有联网就无法进入页面
- Splash布局文件
<TextView android:id="@+id/tv_version" android:textColor="#000000" android:textSize="20sp" android:shadowColor="#ff0000" android:shadowDx="1" android:shadowDy="1" android:shadowRadius="1" android:layout_centerInParent="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="版本 1.0" />
- 获取版本信息
versionName和versionCode的区别和用处 //获取版本信息 private String getVersion() { PackageManager pm = getPackageManager(); try { PackageInfo info = pm.getPackageInfo(getPackageName(), 0); String versionName = info.versionName; int versionCode = info.versionCode; Log.d(TAG, "versionName=" + versionName + "; versionCode=" + versionCode); return versionName; } catch (NameNotFoundException e) { e.printStackTrace(); } return ""; }
- 版本校验
服务器端json数据
{
"version_name": "2.0",
"version_code": 2,
"description": "最新版手机卫士,快来下载体验吧!",
"download_url": "http://10.0.2.2:8080/mobilesafe2.0.apk"
}
注意: 保存文本为 "UTF-8 无BOM" 格式
读取服务器数据流
URL url = new URL(getString(R.string.server_url));
HttpURLConnection conn = (HttpURLConnection) url
.openConnection();
conn.setRequestMethod("GET");// 请求方法
conn.setConnectTimeout(5000);// 请求超时
int code = conn.getResponseCode();
if (code == 200) {
InputStream in = conn.getInputStream();
String result = StreamTools.readFromStream(in);
JSONObject json = new JSONObject(result);
String versionName = json.optString("version_name",
null);
int versionCode = json.getInt("version_code");
String description = json.optString("description");
String downloadUrl = json.getString("download_url");
Log.d(TAG, "description:" + description);
}
/**
* @param is 输入流
* @return String 返回的字符串
* @throws IOException
*/
public static String readFromStream(InputStream is) throws IOException{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer))!=-1){
baos.write(buffer, 0, len);
}
is.close();
String result = baos.toString();
baos.close();
return result;
}
- 更新弹窗
- 页面延时2秒后再跳转
long end = System.currentTimeMillis(); long elapse = end - start; if (elapse < 2000) { try { Thread.sleep(2000 - elapse); } catch (InterruptedException e) { e.printStackTrace(); } } handler.sendMessage(msg);
- 添加AlphaAnimation动画效果
//开启渐变动画 AlphaAnimation anim = new AlphaAnimation(0.3f, 1f); anim.setDuration(2000); rlRoot.startAnimation(anim);
- 下载apk
- 判断SDcard是否挂载代码:
if(Environment.getExternalStorageState().equal(Environment.MEDIA_MOUNTED))
- 使用afinal框架进行下载
FinalHttp fh = new FinalHttp(); fh.download(downloadUrl, localPath, new AjaxCallBack<File>() { @Override public void onLoading(long count, long current) { //下载进度回调 } @Override public void onSuccess(File t) { //下载成功 } @Override public void onFailure(Throwable t, int errorNo, String strMsg) { //下载失败 } });
- 使用xutils框架进行下载
// 下载apk HttpUtils hu = new HttpUtils(); hu.download(downloadUrl, localPath, new RequestCallBack<File>() { @Override public void onLoading(long total, long current, boolean isUploading) { //下载进度回调 } @Override public void onSuccess(ResponseInfo<File> responseInfo) { //下载成功 } @Override public void onFailure(HttpException error, String msg) { //下载失败 } });
- 判断SDcard是否挂载代码:
- 安装apk
查看PackageInstaller源码, 查看AndroidManifest.xml文件中Activity的配置, 从而决定在跳转系统安装界面的Activity时应该传哪些参数.
// 安装apk
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setDataAndType(
Uri.fromFile(t),
"application/vnd.android.package-archive");
startActivity(intent);
安装失败
在Android手机里不允许有两个应用程序有相同的包名;
假设A应用的包名:com.itheima.mobilesafeA;
A应用已经在系统中存在了。
这个时候再去安装一个应用B ,他的报名也叫 con.itheima.mobilesafeA
系统就会去检查这两应用的签名是否相同。如果相同,B会把A给覆盖安装掉;
如果不相同 B安装失败;
要想自动安装成功,必须保证应用程序不同版本的签名完成一样。
- 签名
默认签名
直接在eclipse里运行项目是, 会采用默认签名debug.keystore. 查找方式: Window->Preference->Android->Build, 可以看到默认签名文件的路径, 默认是: C:\Users\tt\.android\debug.keystore
默认签名的特点:
1. 不同电脑,默认签名文件都不一样
2. 有效期比较短, 默认是1年有效期
3. 有默认密码: android, 别名:androiddebugkey
正式签名
正式签名特点:
1. 发布应用市场时, 统一使用一个签名文件
2. 有效期比较长, 一般25年以上
3. 正式签名文件比较重要,需要开发者妥善保存签名文件和密码
使用正式签名文件,分别打包1.0和2.0, 安装运行1.0版本,测试升级是否成功
签名文件丢失后, 肿么办?
1. 让用户卸载旧版本, 重新在应用市场上下载最新版本, 会导致用户流失
2. 更换包名, 重新发布, 会出现两个手机卫士, 运行新版手机卫士, 卸载旧版本
3. 作为一名有经验的开发人员, 最好不要犯这种低级错误!
- 细节处理
- Dialog样式的版本兼容问题
Application主题设置为android:theme="@style/AppTheme" <style name="AppTheme" parent="AppBaseTheme"> <item name="android:windowNoTitle">true</item>//隐藏标题 </style>
- 点击物理返回键的bug
// builder.setCancelable(false);//流氓手段,让用户点击返回键没有作用, 不建议采纳 // 点击物理返回键,取消弹窗时的监听 builder.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { enterHome(); } });
- getApplicationContext和Activity.this的区别
Context是Activity的父类 父类有的方法, 子类一定有, 子类有的方法,父类不一定有 当show一个Dialog时, 必须传Activity对象, 否则会出异常 android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application 因为Dialog必须依赖Activity为载体才能展示出来, 所以必须将Activity对象传递进去 以后在使用Context的时候, 尽量传递Activity对象, 这样比较安全
- 用户取消安装apk, 卡死在Splash页面
在跳转系统安装页面时,startActivityForResult(intent, 0), 在onActivityResult中跳转主页面
- Dialog样式的版本兼容问题
- 主页面GridView搭建
<!--标题--> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:text="功能列表" android:background="#8866ff00" android:textSize="22sp" android:gravity="center" android:orientation="vertical" >
<GridView android:id="@+id/gv_home" android:layout_width="match_parent" android:layout_height="match_parent" android:numColumns="3" android:verticalSpacing="15dp"> </GridView>
- 自定义获取焦点的TextView,走马灯效果
// 让系统认为,当前控件一直处于获取焦点的状态 @Override public boolean isFocused() { return true; } <com.itheima.mobilesafeteach.ui.FocusedTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true" android:ellipsize="marquee" android:textSize="16sp" android:textColor="#000000" android:text="我是您的手机安全卫士, 我会时刻保护您手机的安全! 啊哈哈哈哈" />
- 自定义组合控件SettingItemView
- 布局文件中完成item样式
- 创建自定义SettingItemView,继承RelativeLayout, 在构造方法中完成布局加载
- 设置item点击事件,Checkbox切换,文字变化
- 在SP中记录item状态, 在SplashActivity中判断item状态,决定是否升级
- 思维导图总结
Day02
-
SettingItemView自定义属性
- 删除代码中对文本的动态设置, 改为在布局文件中设置
- 在布局文件中增加新的命名空间
xmlns:itheima="http://schemas.android.com/apk/res/com.itheima.mobilesafeteach"
- 参照系统源码attrs.xml, 找到定义TextView属性的位置,拷贝相关代码
-
创建attrs.xml, 定义相关属性
<!-- 自定义属性 --> <declare-styleable name="SettingItemView"> <attr name="title" format="string" /> <attr name="desc_on" format="string" /> <attr name="desc_off" format="string" /> </declare-styleable>
- 读取自定义属性的值, 更新TextView的内容
int count = attrs.getAttributeCount(); for (int i = 0; i < count; i++) { Log.d("Test", "name=" + attrs.getAttributeName(i) + "; value=" + attrs.getAttributeValue(i)); } String title = attrs.getAttributeValue(NAMESPACE, "title");
- 自定义组合控件小结
1.声明一个View对象 继承相对布局,或者线性布局 或者其他的ViewGroup 2.在自定义的View对象里面重写它的构造方法。在构造方法里面就把布局都初始化完毕 3.根据业务需求 添加一些api方法,扩展自定义的组合控件 4.布局文件里面 可以自定义一些属性 1) res/values/attrs.xml,定义属性 2) 在布局文件中,定义命名空间 3) 给控件设置自定义属性 4) 在自定义控件中,获取属性的值
- 防盗模块自定义对话框, 低版本样式适配
1.检测密码是否已经设置, 弹出设置密码框或输入密码框
2.自定义对话框布局文件
<TextView android:layout_width="match_parent" android:layout_height="40dp" android:background="#66ff6600" android:gravity="center" android:orientation="vertical" android:text="设置密码" android:textSize="20sp" > </TextView> <EditText android:id="@+id/et_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textPassword" />
- 按钮响应时间处理
对比密码是否相同, 相同的话保存在sp中,进入手机防盗页,否则给错误提示
-
2.3版本对话框有黑色背景, 需要进行适配
首先, 将布局文件的整体背景设置为白色 android:background="#ffffff" 其次, 为了去掉残余边框, 需要将布局的边距设置为0 alertDialog = builder.create(); alertDialog.setView(view, 0, 0, 0, 0); alertDialog.show();
- 按钮响应时间处理
- Root权限介绍
使用命令行adb shell进入linux环境, cd切换目录,cat展示文本信息 模拟器可以直接看到data/data目录下的文件, 因为模拟器本身已经具有root权限. 但是市面上出售的真机为了保护app的信息,增强系统安全性,默认是不具备root权限的,而且无法直接看到data/data目录的文件. 拥有了root权限, 就相当于是一个超级管理员,可以随意修改手机内的任何文件. 为了获取手机root权限, 可以使用刷机工具, 比如刷机大师等进行一键root. 辨认手机是否root的方法: 1. 查看data/data目录是否能够访问,能访问就是root状态 2. 打开命令行工具, adb shell进入linux环境, 显示#号表示已经root, 显示$表示没有root
- md5介绍
为了安全保存密码, 使用到了md5算法, md5是一种不可逆的加密算法 public static void main(String[] args) { try { String password = "123456"; MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] result = digest.digest(password.getBytes()); StringBuffer sb = new StringBuffer(); for (byte b : result) { int i = b & 0xff;// 将字节转为整数 String hexString = Integer.toHexString(i);// 将整数转为16进制 if (hexString.length() == 1) { hexString = "0" + hexString;// 如果长度等于1, 加0补位 } sb.append(hexString); } System.out.println(sb.toString());//打印得到的md5 } catch (NoSuchAlgorithmException e) { // 如果算法不存在的话,就会进入该方法中 e.printStackTrace(); } }
登录网站: http://www.cmd5.com/ 验证md5准确性
演示md5如何暴力破解
为避免暴力破解, 可以对算法加盐
什么是加盐?
比如以前我们只是把password进行md5加密, 现在可以给password加点盐,这个盐可以是一个固定的字符串,比如用户名username, 然后我们计算一下md5(username+password), 保存在服务器的数据库中, 即使这个md5泄露, 被人破解后也不是原始的密码, 一定程度上增加了安全性
合并代码进入工程(MD5Utils)方法名:encode
删除config.xml (rm *),并演示. 系统通常对sp有缓存,所以有必要把程序卸载掉之后再进行重试
- 设置向导
演示搜狗输入法设置向导
完成第一个向导页面Setup1Activity的布局文件
- style样式介绍
标题样式:
<style name="text_title_style">
<item name="android:background">#8866ff00</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">50dp</item>
<item name="android:orientation">vertical</item>
<item name="android:textSize">22sp</item>
<item name="android:gravity">center</item>
</style>
文本样式:
<style name="text_content_style">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginLeft">5dp</item>
<item name="android:layout_marginTop">5sp</item>
<item name="android:textColor">#000000</item>
<item name="android:textSize">18sp</item>
<item name="android:gravity">center_vertical</item>
</style>
- 用到的系统图片
android:drawableLeft="@android:drawable/star_big_on"//五角星
android:src="@android:drawable/presence_online" //小点选中
android:src="@android:drawable/presence_invisible" //小点不选中
- selector介绍
1. 查看系统style.xml中有关Button样式的描述, 寻找Button的背景xml <style name="Widget.Holo.Light.Button" parent="Widget.Button"> 2. 查看谷歌官方文档, 了解selector的详细设置方法 App Resources>Resource Types>Drawable>State List 拷贝Example的代码,在项目中运行.使用美图秀秀作图 50*50 3. 使用准备好的图片创建新的selector, 设置给引导页面和Dailog
- 完成4个设置引导页
1. Button 样式统一style 2. 上一页和下一页逻辑处理
- 完成手机防盗页布局
“重新进入设置向导” 按钮样式调整, 使用TextView添加selector,
android:clickable=”true”, 处理该按钮的点击事件
- Shape介绍
1. 查看官方文档有关Shape的介绍 App Resources>Resource Types>Drawable>Shape Drawable 拷贝Example的代码,在项目中运行 2. 演示shape下的几个属性 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > <!-- 圆角弧度 --> <corners android:radius="5dp" /> <!-- 渐变 <gradient android:startColor="#ff0000" android:endColor="#00ff00" /> --> <!-- 填充色 --> <solid android:color="#fff" /> <!-- 边框(虚线) <stroke android:width="1dp" android:color="#000000" android:dashWidth="8dp" android:dashGap="3dp"/> --> </shape>
- Activity切换动画
下一页动画
trans_in.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromXDelta="100%p" android:toXDelta="0"
android:duration="500"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
trans_out.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="500"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
上一页动画
trans_pre_in.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="500"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
trans_pre_out.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromXDelta="0" android:toXDelta="100%p"
android:duration="500"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
Activity切换的动画效果
overridePendingTransition(R.anim.trans_in, R.anim.trans_out);//Activity切换的动画效果
- 手势识别器
detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (Math.abs(e1.getRawY() - e2.getRawY()) > 100) { Toast.makeText(BaseSetupActivity.this, "不能这样划哦!", Toast.LENGTH_SHORT).show(); return true; } if (Math.abs(velocityX) < 100) { Toast.makeText(BaseSetupActivity.this, "速度太慢啦!", Toast.LENGTH_SHORT).show(); return true; } if (e2.getRawX() - e1.getRawX() > 200) { Log.d("Test", "显示上一页"); showPrevious(); return true; } if (e1.getRawX() - e2.getRawX() > 200) { Log.d("Test", "显示下一页"); showNext(); return true; } return super.onFling(e1, e2, velocityX, velocityY); } }); @Override public boolean onTouchEvent(MotionEvent event) { detector.onTouchEvent(event); return super.onTouchEvent(event); }
- 代码重构, 抽取父类
BaseSetupActivity
// 展示下一页
public abstract void showNext();
// 展示上一页
public abstract void showPrevious();
// 下一页按钮点击
public void next(View view) {
showNext();
}
// 上一页按钮点击
public void previous(View view) {
showPrevious();
}
Day03
- 手机防盗流程梳理
-
sim卡绑定页面实现(Setup2Activity)
TelephonyManager mTelePhonyManager; mTelePhonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); String simSerialNumber = mTelePhonyManager.getSimSerialNumber();// 获取sim卡序列号 需要权限: <uses-permission android:name="android.permission.READ_PHONE_STATE"/> 将序列号保存在sp中,根据sp是否有值来更新选择框状态
- 监听开机启动,检测sim卡变化
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <receiver android:name=".receiver.BootCompleteReceiver" > <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> 如果发现当前sim卡和sp中保存的不一致,需要向安全号码发送报警短信
- 读取联系人Demo
/** * 读取联系人 */ private ArrayList<HashMap<String, String>> readContacts() { ArrayList<HashMap<String, String>> contacts = new ArrayList<HashMap<String, String>>(); ContentResolver resolver = getContentResolver(); Uri uriRaw = Uri.parse("content://com.android.contacts/raw_contacts");// raw_contacts表的uri Uri uriData = Uri.parse("content://com.android.contacts/data");// data表的uri Cursor cursor = resolver.query(uriRaw, new String[] { "contact_id" }, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { String id = cursor.getString(0); Cursor dataCursor = resolver.query(uriData, new String[] { "data1", "mimetype" }, "raw_contact_id=?", new String[] { id }, null); if (dataCursor != null) { HashMap<String, String> map = new HashMap<String, String>(); while (dataCursor.moveToNext()) { String data = dataCursor.getString(0); String mimeType = dataCursor.getString(1); if ("vnd.android.cursor.item/phone_v2".equals(mimeType)) { map.put("phone", data);// 设置手机号码 } else if ("vnd.android.cursor.item/name" .equals(mimeType)) { map.put("name", data);// 设置名称 } } contacts.add(map); } } } return contacts; } SimpleAdapter adapter = new SimpleAdapter(this, contacts, R.layout.list_contact_item, new String[] { "name", "phone" }, new int[] { R.id.tv_name, R.id.tv_phone }); lvList.setAdapter(adapter); 需要配置权限 <uses-permission android:name="android.permission.READ_PHONE_STATE" />
- 将联系人模块导入到项目中, 点击”选择联系人”,跳转到联系人列表页
通过startActivityForResult方式跳转,可以获取联系人页面的回传数据 SelectContactActivity: Intent intent = new Intent(); intent.putExtra("phone", phone); setResult(Activity.RESULT_OK, intent); finish(); ------------------- Setup3Activity: @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { System.out.println("onActivityResult:" + resultCode); if (resultCode == Activity.RESULT_OK) { String phone = data.getStringExtra("phone"); phone = phone.replace("-", "");//去掉"-" phone = phone.replace(" ", "");//去掉空格 etPhoneNumber.setText(phone); } super.onActivityResult(requestCode, resultCode, data); } <EditText android:id="@+id/et_phone_number" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="phone"//设定键盘类型为电话号码 android:hint="请输入或选择安全号码" > //如果安全号码不为空,更新EditText String phone = mSp.getString("safe_phone", null); if (!TextUtils.isEmpty(phone)) { etPhoneNumber.setText(phone); } //跳转下一个页面 String phone = etPhoneNumber.getText().toString().trim();// 过滤掉两侧空格后,获取号码信息 if (TextUtils.isEmpty(phone)) { Toast.makeText(this, "必须设定安全号码!", Toast.LENGTH_SHORT).show(); return; } mSp.edit().putString("safe_phone", phone).commit();// 保存电话号码
- 防盗保护页面状态更新(LostFindActivity)
//判断防盗保护是否开启,更新图标状态 boolean protecting = sp.getBoolean("protecting", false); if (protecting) { ivProtect.setImageResource(R.drawable.lock); } else { ivProtect.setImageResource(R.drawable.unlock); } tvSafePhone.setText(sp.getString("safe_phone", ""));//更新安全号码
- 开机启动后发送提醒短信
System.out.println("sim卡发生变化啦!!!!我要报警!!!"); String phone = sp.getString("safe_phone", ""); //向安全号码发送短信 SmsManager sms = SmsManager.getDefault(); sms.sendTextMessage(phone, null, "sim card changed!!!", null, null);
- 开启两个模拟器,演示sim卡变化,发送短信的效果
1. 在BootCompleteReceiver修改获取的sim卡序列号,导致和sp中不一致,运行在A模拟器上 2. 开启另外一个模拟器B,将该模拟器的号码设定为A的安全号码 3. 重启A模拟器, 查看B是否收到报警短信
- 拦截短信
<receiver android:name="com.itheima.mobilesafe.receiver.SmsReceiver" > <intent-filter android:priority="2147483647" > <action android:name="android.provider.Telephony.SMS_RECEIVED" /> </intent-filter> </receiver> public class SmsReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Object[] pdus = (Object[]) intent.getExtras().get("pdus"); for (Object obj : pdus) { SmsMessage msg = SmsMessage.createFromPdu((byte[]) obj); String address = msg.getOriginatingAddress(); String body = msg.getMessageBody(); System.out.println("收到短信了:" + body + ";发信人:" + address); if ("#*location*#".equals(body)) { System.out.println("获取手机地理位置"); abortBroadcast();//中断广播的传递 } else if ("#*alarm*#".equals(body)) { System.out.println("播放报警铃声"); abortBroadcast(); } else if ("#*wipedata*#".equals(body)) { System.out.println("清除手机数据"); abortBroadcast(); } else if ("#*lockscreen*#".equals(body)) { System.out.println("锁屏"); abortBroadcast(); } } } }
- 播放报警音乐
将手机调整为静音, 在静音模式下,演示是否依然能够播放音乐. android手机的声音有多种类型, 音乐&视频&多媒体等, 铃声&通知, 闹铃, 三种类型的音量都是单独控制的, 手机静音只是将铃声&通知的声音调整到最小,不会影响到音乐的音量
MediaPlayer player = MediaPlayer.create(context, R.raw.ylzs);
player.setVolume(1.0f, 1.0f);
player.setLooping(true);
player.start();
- 手机定位
- 网络定位
根据IP显示具体的位置, 原理是建立一个库那个IP地址对应那个地方;早期警方破案就采用此特点; 有局限性:针对固定的IP地址。 如果手机网或者ip地址是动态分布IP,这个偏差就很大。这种情况是无法满足需求的。
- 基站定位
工作原理:手机能打电话,是需要基站的。手机定位也是用基站的。 手机附近能收到3个基站的信号,就可以定位了。 基站定位有可能很准确,比如基站多的地方; 如果基站少的话就会相差很大。 精确度:几十米到几公里不等;
- GPS定位
至少需要3颗卫星; 特点是:需要搜索卫星, 头顶必须是空旷的; 影响条件:云层、建筑、大树。 卫星:美国人、欧洲人的卫星。 北斗:中国的,但没有民用,只是在大巴,战机等使用。 A-GPS: 通过GPS和网络共同定位,弥补GPS的不足, 精确度可达到15米以内
- 网络定位
- 定位Demo演示
lm = (LocationManager) getSystemService(LOCATION_SERVICE);//获取系统位置服务 List<String> allProviders = lm.getAllProviders();//获取所有位置提供者 listener = new MyLocationListener();//位置监听器 lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, listener);//更新位置, 参2和参3设置为0,表示只要位置有变化就立即更新 class MyLocationListener implements LocationListener { //位置发生变化 @Override public void onLocationChanged(Location location) { System.out.println("onLocationChanged"); String longitude = "经度:" + location.getLongitude(); String latitude = "纬度:" + location.getLatitude(); String accuracy = "精确度:" + location.getAccuracy(); String altitude = "海拔:" + location.getAltitude(); tvLocation.setText(longitude + "\n" + latitude + "\n" + accuracy + "\n" + altitude); } //位置提供者的状态发生变化 @Override public void onStatusChanged(String provider, int status, Bundle extras) { System.out.println("onStatusChanged"); } //位置提供者可用 @Override public void onProviderEnabled(String provider) { System.out.println("onProviderEnabled"); } //位置提供者不可用 @Override public void onProviderDisabled(String provider) { System.out.println("onProviderDisabled"); } } @Override protected void onDestroy() { super.onDestroy(); lm.removeUpdates(listener);//为了节省性能,当页面销毁时,删除位置更新的服务 listener = null; } 需要权限: <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>//获取准确GPS坐标的权限 <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>//允许模拟器模拟位置坐标的权限 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>//获取粗略坐标的权限(网络定位时使用)
- 火星坐标
获取到坐标后在谷歌地图上查询,发现坐标有所便宜, 不准确.这是因为中国的地图服务,为了国家安全, 坐标数据都经过了政府加偏处理, 加偏处理后的坐标被称为火星坐标. 技术牛人通过对美国地图和中国地图的比对,生成了一个查询数据库, 专门用与标准坐标和火星坐标的转换.导入数据库文件axisoffset.dat和工具类ModifyOffset.java,创建一个java工程进行演示 public static void main(String[] args) { try { ModifyOffset offset = ModifyOffset.getInstance(Demo.class .getResourceAsStream("axisoffset.dat"));//加载数据库文件 PointDouble s2c = offset.s2c(new PointDouble(116.2821962, 40.0408444));//标准坐标转为火星坐标 System.out.println(s2c); } catch (Exception e) { e.printStackTrace(); } }
- 开启服务,动态存储最新的坐标
LocationService public void onCreate() { lm = (LocationManager) getSystemService(LOCATION_SERVICE);// 获取系统位置服务 Criteria criteria = new Criteria(); criteria.setAccuracy(Criteria.ACCURACY_FINE);// 准确度良好 criteria.setCostAllowed(true);// 是否允许花费(比如网络定位) String bestProvider = lm.getBestProvider(criteria, true);// 获取当前最好的位置提供者 System.out.println("位置提供者:" + bestProvider); listener = new MyLocationListener();// 位置监听器 lm.requestLocationUpdates(bestProvider, 0, 0, listener);// 更新位置, // 参2和参3 设置为0,表示只要位置有变化就立即更新 }; // 位置发生变化 @Override public void onLocationChanged(Location location) { // 保存经纬度信息 SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE); sp.edit() .putString("last_location", longitude + latitude + accuracy) .commit(); stopSelf();// 停止位置服务 } ---------------------------------- SmsReceiver if ("#*location*#".equals(body)) { System.out.println("获取手机地理位置"); context.startService(new Intent(context, LocationService.class));// 开启位置服务 SharedPreferences sp = context.getSharedPreferences("config", Context.MODE_PRIVATE); String location = sp.getString("last_location", null); String reply = location; if (TextUtils.isEmpty(reply)) { reply = "getting location..."; } SmsManager.getDefault().sendTextMessage(address, null, reply, null, null); abortBroadcast();// 中断广播的传递 }
项目演示
开启两个模拟器,发送短信#location#,查看是否可以收到经纬度的短信.第一次发送时,sp中没有保存,返回的是”getting location…”, 为了保证模拟器能更新sp,需要在控制台发送模拟的经纬度信息. LocationService启动后获取经纬度,一旦获取成功,马上停止服务,这样可以节省耗电量. 演示服务开启和结束的场景.
- 超级管理员
Administration官方文档介绍: http://developer.android.com/guide/topics/admin/device-admin.html
网站推荐: http://www.androiddevtools.cn/ 查看中文文档
应用: 锁屏, 清除系统数据
ApiDemo中的案例演示
配置超级管理员步骤:
1. 自定义Receiver,继承DeviceAdminReceiver
2. 配置manifest
<receiver
android:name=".AdminReceiver"
android:description="@string/sample_device_admin_description"
android:label="@string/sample_device_admin"
android:permission="android.permission.BIND_DEVICE_ADMIN" >
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_sample" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
3. 添加配置文件@xml/device_admin_sample
4. 获取DevicePolicyManager
mDPM = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE)
5. 一键锁屏
mDPM.lockNow();//锁屏
mDPM.resetPassword("123", 0);//设置锁屏密码
注意: 必须先打开设置->安全->设备管理器的权限,否则运行崩溃
6. 通过代码打开超级管理员权限
public void openAdmin(View view) {
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
ComponentName component = new ComponentName(this, AdminReceiver.class);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, component);
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
"打开超级管理员权限,可以一键锁屏,删除数据等");
startActivity(intent);
}
7. 验证是否已经激活设备管理员
ComponentName component = new ComponentName(this, AdminReceiver.class);
if (mDPM.isAdminActive(component)) {
}
8. 桌面应用,一键锁屏, 应用市场搜索
9. 如何卸载应用
public void uninstall(View view) {
ComponentName component = new ComponentName(this, AdminReceiver.class);
mDPM.removeActiveAdmin(component);//删除超级管理权限
//跳转到卸载页面
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
10. 清除数据
//mDPM.wipeData(0);//恢复出厂设置
//mDPM.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE);//清除sdcard内容
当收到锁屏或清除数据的短信时, 介绍处理步骤(留给学生自己去实现)
Day04
- 高级工具
AToolsActivity 布局文件: <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/button" android:drawableLeft="@android:drawable/ic_menu_camera" android:drawablePadding="5dp" android:gravity="center_vertical" android:onClick="numberAddressQuery" android:padding="5dp" android:clickable="true" android:text="电话归属地查询" > </TextView>
- 号码归属地查询
NumberAddressActivity
- 原理分析
- 网络查询(百度搜索手机归属地查询)
- 数据库查询(数据库可以从网上下载,也可从网络购买)
- sqlite导入本地数据库
- 原始数据库, 有很多地名重复,可以进一步优化
将地名和卡类型的数据单独导入一张表中, 再将手机号前缀导入另外一张表,通过外键查询,数据量大大减小 select area,city,cardtype from info group by area,city,cardtype
- 小米数据库
1. 根据号码前7位查询外键 select outkey from data1 where id="1861094" 2. 根据外键查询位置信息 select area,location from data2 where id=91 3. 组合查询,直接根据号码前7位查询位置信息 select area,location from data2 where id=(select outkey from data1 where id="1861094")
- 原始数据库, 有很多地名重复,可以进一步优化
- 原理分析
- 拷贝数据库
SQLiteDatabase不支持直接从assets读取文件,所以要提前拷贝数据库
NumberAddressDao public static final String PATH = "data/data/com.itheima.mobilesafeteach/files/address.db"; SQLiteDatabase db = SQLiteDatabase.openDatabase(PATH, null, SQLiteDatabase.OPEN_READONLY); --------------------------------- SplashActivity 更新版本前,先拷贝数据库address.db /** * 拷贝数据库 */ private void copyDB(String dbName) { File file = new File(getFilesDir(), dbName);// 目的文件 if (file.exists()) { System.out.println("数据库" + dbName + "已存在,无须拷贝!"); return; } FileOutputStream out = null; InputStream in = null; try { out = new FileOutputStream(file); in = getAssets().open(dbName);// 源文件 int len = 0; byte[] buffer = new byte[1024]; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { try { out.close(); in.close(); } catch (Exception e) { e.printStackTrace(); } } }
- 查询数据库
Cursor cursor = db .rawQuery( "select location from data2 where id=(select outkey from data1 where id=?)", new String[] { number.substring(0, 7) }); if (cursor != null) { if (cursor.moveToNext()) { address = cursor.getString(0); System.out.println("address:" + address); } cursor.close(); }
- 号码合法性判断
正则表达式
手机号: “^1[345678]\d{9}$”; 数字: “^\d+$”
- 特殊号码判断
switch (number.length()) {
case 3:
// 匪警电话 ,110,120等
address = "报警电话";
break;
case 4:
// 模拟器电话,5554,5556
address = "模拟器";
break;
case 5:
// 客服电话,95555
address = "客服电话";
break;
case 7:
case 8:
// 本地电话
address = "本地电话";
break;
- 座机判断
if (number.startsWith("0") && number.length() > 10) {// 座机号码
Cursor cursor = db.rawQuery(
"select location from data2 where area=?",
new String[] { number.substring(1, 4) });
if (cursor.moveToNext()) {// 先查询前4位
address = cursor.getString(0);
}
cursor.close();
if (TextUtils.isEmpty(address)) {// 如果前4位没有数据,就查询前3位
cursor = db.rawQuery(
"select location from data2 where area=?",
new String[] { number.substring(1, 3) });
if (cursor.moveToNext()) {
address = cursor.getString(0);
}
cursor.close();
}
}
注意: db.close();//关闭数据库
- 监听文字变化,动态查询
etNumber.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { System.out.println("onTextChanged"); if (s.length() >= 3) { String address = NumberAddressDao.getAddress(s.toString()); if (!TextUtils.isEmpty(address)) { tvResult.setText(address); } else { tvResult.setText("无结果"); } } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { System.out.println("beforeTextChanged"); } @Override public void afterTextChanged(Editable s) { System.out.println("afterTextChanged"); } });
- 抖动效果
- 引入ApiDemo,查找抖动效果的代码
- 拷贝相关代码到自己的项目中,运行
- 代码解读,插补器介绍
结合Interpolator的子类,如线性插补器和循环插补器的源码来分析,更容易理解 Animation shake = AnimationUtils.loadAnimation(this, R.anim.shake); shake.setInterpolator(new Interpolator() { @Override public float getInterpolation(float x) { float y = x;//线程插补器 return y; } });
- 振动效果
private void vibrate() { Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); // vibrator.vibrate(2000);//震动2秒 long[] pattern = new long[] { 1000, 2000, 1000, 3000 };// 先等待1秒,再震动2秒,再等待1秒,再震动3秒... vibrator.vibrate(pattern, -1);// 参2等于-1时,表示不循环,大于等于0时,表示从以上数组的哪个位置开始循环 } 注意权限: <uses-permission android:name="android.permission.VIBRATE"/>
按摩器app就是基于震动的api开发的
- 来电监听
创建后台服务 AddressService public void onCreate() { listener = new MyPhoneListener(); tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); tm.listen(listener, PhoneStateListener.LISTEN_CALL_STATE); }; @Override public void onDestroy() { super.onDestroy(); tm.listen(listener, PhoneStateListener.LISTEN_NONE); listener = null; } class MyPhoneListener extends PhoneStateListener { @Override public void onCallStateChanged(int state, String incomingNumber) { switch (state) { case TelephonyManager.CALL_STATE_RINGING: String address = NumberAddressDao.getAddress(incomingNumber); Toast.makeText(AddressService.this, address, Toast.LENGTH_LONG) .show(); break; default: break; } super.onCallStateChanged(state, incomingNumber); } } 设置页面新增勾选框,点击后启动或停止service
- 判断服务是否在后台运行,更新checkbox
public static boolean isServiceRunning(String serviceName, Context ctx) { ActivityManager am = (ActivityManager) ctx .getSystemService(Context.ACTIVITY_SERVICE); List<RunningServiceInfo> runningServices = am.getRunningServices(100);//获取所有后台运行的服务 for (RunningServiceInfo runningServiceInfo : runningServices) { String className = runningServiceInfo.service.getClassName(); if (className.equals(serviceName)) { return true; } } return false; }
- 去电监听
- 静态注册广播
<receiver android:name=".receiver.OutCallReceiver" > <intent-filter> <action android:name="android.intent.action.NEW_OUTGOING_CALL" /> </intent-filter> </receiver> 注意添加权限: <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/> 问题: 当开关关闭时,仍然能显示去电地址信息
- 动态注册广播
当启动后台服务时,注册广播,服务停止后,注销广播,这样的话,来电和去电的位置显示都可以由一个开关来控制
- 静态注册广播
-
自定义Toast
- Toast原理分析
查找transient_notification文件,查看布局样式, 在values/themes中搜索toastFrameBackground, 查看背景图片toast_frame.9.png 分析Toast源码, 创建自定义Toast private void showToast(String address) { view = new TextView(this); view.setText(address); view.setTextColor(Color.RED); final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; params.format = PixelFormat.TRANSLUCENT; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); wm.addView(view, params); } 监听电话状态, 如果电话处于空闲状态,就从WindowManager中删除View case TelephonyManager.CALL_STATE_IDLE: if (wm != null && view != null) { wm.removeView(view); } break;
- 金山手机卫士
演示金山手机卫士归属地样式, 模仿其样式进行开发. 解压金山手机卫士apk,获取相关资源文件. 注意: 相关图片在drawable目录下, 而非drawable-hdpi
- 自定义Toast样式
1. 布局文件 电话图标: @android:drawable/ic_menu_call 2. 自定义SettingClickView, 类似SettingItemView 去掉自定义属性,保留setDesc和setTitle两个方法 3. 初始化SettingClickView, 设置点击事件,弹出单选Dialog // 选择归属地样式的弹窗 AlertDialog.Builder builder = new AlertDialog.Builder( SettingActivity.this); int style = sp.getInt("address_style", 0); builder.setSingleChoiceItems(items, style, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { sp.edit().putInt("address_style", which) .commit(); scvStyle.setDesc(items[which]); dialog.dismiss(); } }); builder.setNegativeButton("取消", null); builder.show(); 4. 选择相应样式,保存在sp中 5. 从sp中读取样式,在AddressService中更改背景图片 SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE); int style = sp.getInt("address_style", 0); int[] bgs = new int[] { R.drawable.call_locate_white, R.drawable.call_locate_orange, R.drawable.call_locate_blue, R.drawable.call_locate_gray, R.drawable.call_locate_green }; view.setBackgroundResource(bgs[style]);
- Toast原理分析
- 修改归属地显示位置
定义DragViewActivity 1. 布局文件: <RelativeLayout 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" android:orientation="vertical" > <TextView android:id="@+id/tv_top" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:background="@drawable/call_locate_blue" android:gravity="center" android:text="按住提示框拖动到任意位置,按手机返回键立刻生效" android:textColor="#000" android:textSize="20sp" /> <TextView android:id="@+id/tv_bottom" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:background="@drawable/call_locate_blue" android:gravity="center" android:text="按住提示框拖动到任意位置,按手机返回键立刻生效" android:textColor="#000" android:textSize="20sp" android:visibility="invisible" /> <ImageView android:id="@+id/iv_drag" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="90dp" android:src="@drawable/drag" /> </RelativeLayout> 2. 拖拽事件监听 ivDrag.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //获取起始点坐标 startX = (int) event.getRawX(); startY = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int endX = (int) event.getRawX(); int endY = (int) event.getRawY(); int dx = endX - startX; int dy = endY - startY; System.out.println("位置偏移:(" + dx + "," + dy + ")"); //根据手指的移动偏移量,计算出图片相应的位置 int left = ivDrag.getLeft() + dx; int top = ivDrag.getTop() + dy; int right = ivDrag.getRight() + dx; int bottom = ivDrag.getBottom() + dy; //判断图片是否移出屏幕 if (left < 0 || right > windowWidth || top < 0 || bottom > windowHeight - 20) { break; } //判断图片位于屏幕上半部分还是下半部分 if (top > windowHeight / 2) { tvBottom.setVisibility(View.INVISIBLE); tvTop.setVisibility(View.VISIBLE); } else { tvBottom.setVisibility(View.VISIBLE); tvTop.setVisibility(View.INVISIBLE); } //重新设定图片的位置 ivDrag.layout(left, top, right, bottom); //重新获取起始点坐标(由于位置已经发生了变化,所以需要重新设定坐标,方便下次移动时读取) startX = (int) event.getRawX(); startY = (int) event.getRawY(); break; case MotionEvent.ACTION_UP: //记录拖拽结束后的坐标点 Editor edit = sp.edit(); edit.putInt("lastX", ivDrag.getLeft()); edit.putInt("lastY", ivDrag.getTop()); edit.commit(); break; default: break; } return true; } }); ----------------- 获取屏幕宽高 WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); final int windowWidth = wm.getDefaultDisplay().getWidth(); final int windowHeight = wm.getDefaultDisplay().getHeight(); 3. 初始化图片位置 LayoutParams params = (LayoutParams) ivDrag.getLayoutParams(); params.leftMargin = lastX; params.topMargin = lastY; ivDrag.setLayoutParams(params); if (lastY > windowHeight / 2) { tvBottom.setVisibility(View.INVISIBLE); tvTop.setVisibility(View.VISIBLE); } else { tvBottom.setVisibility(View.VISIBLE); tvTop.setVisibility(View.INVISIBLE); } 注意:此处不能使用该方法: ivDrag.layout(lastX, lastY, lastX + ivDrag.getWidth(), lastY + ivDrag.getHeight()); 因为当前还没有测量好, 所以不能直接调用layout. 顺序是measure,layout,ondraw
- 使用WindowManager设置归属地位置
int lastX = sp.getInt("lastX", 0); int lastY = sp.getInt("lastY", 0); params.gravity = Gravity.TOP + Gravity.LEFT; //注意要将重心设置在左上方,默认位于屏幕中央 params.x = lastX; params.y = lastY;
- Activity设置透明
1. activity设置主题样式: android:theme="@android:style/Theme.Translucent.NoTitleBar 2. 根布局设置半透明 android:background="#5000"
Day05
- 双击事件
/** * 双击 * @param view */ public void onClick(View view) { if (firstClickTime > 0) { if (System.currentTimeMillis() - firstClickTime < 500) { System.out.println("双击"); firstClickTime = 0; } } firstClickTime = System.currentTimeMillis(); }
- 多击事件
设置->关于手机->"Android 版本",多次点击后会跳转页面 查看系统源码Settings, 搜索"Android 版本"字符串,查找相关代码,拷贝到自己的项目中 long[] mHits = new long[3];//数组长度为点击次数 /** * 多次点击 * * @param view */ public void onClick(View view) { // src 源数组 // srcPos 开始拷贝的位置 // dst 目标数组 // dstPos 目标数组的起始拷贝位置 // length 拷贝的数组长度 System.arraycopy(mHits, 1, mHits, 0, mHits.length - 1);//拷贝数组 mHits[mHits.length - 1] = SystemClock.uptimeMillis(); if (mHits[0] >= (SystemClock.uptimeMillis() - 500)) { System.out.println("是男人!!!"); mHits = new long[3]; } }
- 双击居中
//图片设置为屏幕居中 ivDrag.layout(windowWidth / 2 - ivDrag.getWidth() / 2, ivDrag.getTop(), windowWidth / 2 + ivDrag.getWidth() / 2, ivDrag.getBottom()); //在sp中记录位置 Editor edit = sp.edit(); edit.putInt("lastX", ivDrag.getLeft()); edit.putInt("lastY", ivDrag.getTop()); edit.commit(); 注意: 为了能响应点击事件,需要在onTouch中返回false,将事件传递给onClick
- 窗体触摸移动
1. 为了获取触摸事件,首先需要去掉WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 2. 其次设置params.type = WindowManager.LayoutParams.TYPE_Phone; 3. 增加权限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> 4. 移动逻辑处理 case MotionEvent.ACTION_MOVE: int dx = (int) (event.getRawX() - startX); int dy = (int) (event.getRawY() - startY); params.x += dx; params.y += dy; //控制图片不要超出屏幕边界 if (params.x < 0) { params.x = 0; } //控制图片不要超出屏幕边界 if (params.y < 0) { params.y = 0; } //控制图片不要超出屏幕边界 if (params.x > wm.getDefaultDisplay().getWidth() - view.getWidth()) { params.x = wm.getDefaultDisplay().getWidth() - view.getWidth(); } //控制图片不要超出屏幕边界 if (params.y > wm.getDefaultDisplay().getHeight() - view.getHeight()) { params.y = wm.getDefaultDisplay().getHeight() - view.getHeight(); } System.out.println("当前位置:" + params.x + ";" + params.y); wm.updateViewLayout(view, params);//更新图片的显示位置 startX = (int) event.getRawX(); startY = (int) event.getRawY(); break;
- 火箭发射系统
- 写主页面, 控制service的启动和停止
- 写RocketService, 专用于火箭显示和移动
public class RocketService extends Service { private WindowManager wm; private ImageView ivRocket; private View view; private int startX; private int startY; private WindowManager.LayoutParams params; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); wm = (WindowManager) getSystemService(WINDOW_SERVICE); view = View.inflate(this, R.layout.view_rocket, null);//初始化界面 ivRocket = (ImageView) view.findViewById(R.id.iv_rocket);//获取火箭对象 ivRocket.setBackgroundResource(R.drawable.rocket); AnimationDrawable anim = (AnimationDrawable) ivRocket.getBackground(); anim.start();//启动火箭的帧动画 params = new WindowManager.LayoutParams(); params.gravity = Gravity.TOP + Gravity.LEFT; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; params.format = PixelFormat.TRANSLUCENT; params.type = WindowManager.LayoutParams.TYPE_PHONE; wm.addView(view, params); ivRocket.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // System.out.println("被触摸了"); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = (int) event.getRawX(); startY = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int dx = (int) (event.getRawX() - startX); int dy = (int) (event.getRawY() - startY); params.x += dx; params.y += dy; wm.updateViewLayout(view, params);// 更新图片的显示位置 startX = (int) event.getRawX(); startY = (int) event.getRawY(); break; case MotionEvent.ACTION_UP: // 发射火箭 if (params.x > 100 && params.x < 300 && params.y > 320) { sendRocket(); //启动BackgroundActivity,展示烟雾 Intent intent = new Intent(RocketService.this, BackgroundActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } break; default: break; } return true; } }); } private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { int y = msg.arg1; params.y = y; wm.updateViewLayout(view, params);// 更新图片的显示位置 }; }; /** * 发射火箭 */ protected void sendRocket() { Toast.makeText(RocketService.this, "发射火箭", Toast.LENGTH_SHORT).show(); //将火箭位置调整为屏幕居中 params.x = wm.getDefaultDisplay().getWidth() / 2 - ivRocket.getWidth() / 2; wm.updateViewLayout(view, params); //执行循环, 每隔一段时间发送当前y坐标, 更新界面 new Thread() { @Override public void run() { int pos = 380; for (int i = 0; i < 11; i++) { try { Thread.sleep(50);//调整改时间,可以控制火箭速度 } catch (InterruptedException e) { e.printStackTrace(); } int y = pos - 38 * i; Message msg = Message.obtain(); msg.arg1 = y; mHandler.sendMessage(msg); } } }.start(); } @Override public void onDestroy() { super.onDestroy(); wm.removeView(view); view = null; } }
- 写BackgroundActivity, 用于烟雾的展示
public class BackgroundActivity extends Activity { private ImageView ivSmokeBottom; private ImageView ivSmokeTop; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_background); ivSmokeBottom = (ImageView) findViewById(R.id.iv_smoke_m); ivSmokeTop = (ImageView) findViewById(R.id.iv_smoke_t); // 初始化烟雾 ivSmokeTop.setVisibility(View.VISIBLE); ivSmokeBottom.setVisibility(View.VISIBLE); //初始化渐变动画 AlphaAnimation animAlpha = new AlphaAnimation(0, 1); animAlpha.setFillAfter(true); animAlpha.setDuration(1000); ivSmokeTop.startAnimation(animAlpha); ivSmokeBottom.startAnimation(animAlpha); //1秒之后,销毁activity new Handler().postDelayed(new Runnable() { @Override public void run() { finish(); } }, 1000); } }
- 帧动画介绍
翻看谷歌文档: API Guide->Animation And Graphics-> Drawable Animation 拷贝相关代码, 运行程序
Day06
- 来电短信黑名单拦截
- 演示金山卫士相关功能
- 创建CallSmsSafeActivity
- 布局文件
<RelativeLayout android:layout_width="match_parent" android:layout_height="50dp" android:background="#8866ff00" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:text="黑名单管理" android:textColor="#000" android:textSize="22sp" /> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="5dp" android:text="添加" />
-
数据库创建
public class BlackNumberOpenHelper extends SQLiteOpenHelper {
public BlackNumberOpenHelper(Context ctx) { super(ctx, "blacknumber.db", null, 1);//必须实现该构造方法 } /** * 第一次创建数据库 */ @Override public void onCreate(SQLiteDatabase db) { // 创建表, 三个字段,_id, number(电话号码),mode(拦截模式:电话,短信,电话+短信) db.execSQL("create table blacknumber (_id integer primary key autoincrement, number varchar(20), mode integer)"); } /** * 数据库升级 */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
- 单元测试
- 创建具备单元测试的Android项目, 拷贝清单文件的相关代码
<instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.itheima.mobilesafeteach" /> <application> <uses-library android:name="android.test.runner" /> </application>
- 创建具备单元测试的Android项目, 拷贝清单文件的相关代码
- 增删改查(crud)逻辑实现
/** * 黑名单数据库封装 * @author Kevin * */ public class BlackNumberDao { private static BlackNumberDao sInstance; private BlackNumberOpenHelper mHelper; private BlackNumberDao(Context ctx) { mHelper = new BlackNumberOpenHelper(ctx); }; /** * 获取单例对象 * @param ctx * @return */ public static BlackNumberDao getInstance(Context ctx) { if (sInstance == null) { synchronized (BlackNumberDao.class) { if (sInstance == null) { sInstance = new BlackNumberDao(ctx); } } } return sInstance; } /** * 增加黑名单 * @param number * @param mode */ public void add(String number, int mode) { SQLiteDatabase db = mHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("number", number); values.put("mode", mode); db.insert("blacknumber", null, values); db.close(); } /** * 删除黑名单 * @param number */ public void delete(String number) { SQLiteDatabase db = mHelper.getWritableDatabase(); db.delete("blacknumber", "number=?", new String[] { number }); db.close(); } /** * 更新黑名单 * @param number * @param mode */ public void update(String number, int mode) { SQLiteDatabase db = mHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("mode", mode); db.update("blacknumber", values, "number=?", new String[] { number }); db.close(); } /** * 查找黑名单 * @param number * @return */ public boolean find(String number) { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db.query("blacknumber", new String[] { "number", "mode" }, "number=?", new String[] { number }, null, null, null); boolean result = false; if (cursor.moveToFirst()) { result = true; } cursor.close(); db.close(); return result; } /** * 查找号码拦截模式 * @param number * @return */ public int findMode(String number) { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db.query("blacknumber", new String[] { "mode" }, "number=?", new String[] { number }, null, null, null); int mode = -1; if (cursor.moveToFirst()) { mode = cursor.getInt(0); } cursor.close(); db.close(); return mode; } /** * 查找黑名单列表 * @return */ public ArrayList<BlackNumberInfo> findAll() { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db .query("blacknumber", new String[] { "number", "mode" }, null, null, null, null, null); ArrayList<BlackNumberInfo> list = new ArrayList<BlackNumberDao.BlackNumberInfo>(); while (cursor.moveToNext()) { String number = cursor.getString(0); int mode = cursor.getInt(1); BlackNumberInfo info = new BlackNumberInfo(); info.number = number; info.mode = mode; list.add(info); } cursor.close(); db.close(); return list; } /** * 黑名单对象 * @author Kevin * */ public class BlackNumberInfo { public String number; public int mode; @Override public String toString() { return "BlackNumberInfo [number=" + number + ", mode=" + mode + "]"; } } }
- 增删改查单元测试
public class TestBlackNumberDao extends AndroidTestCase { /** * 测试数据创建 */ public void testCreateDb() { BlackNumberOpenHelper helper = new BlackNumberOpenHelper(getContext()); helper.getWritableDatabase(); } /** * 测试增加黑名单 */ public void testAdd() { //添加100个号码,拦截模式随机 Random random = new Random(); for (int i = 0; i < 100; i++) { int mode = random.nextInt(3) + 1; if (i < 10) { BlackNumberDao.getInstance(getContext()).add("1381234560" + i, mode); } else { BlackNumberDao.getInstance(getContext()).add("138123456" + i, mode); } } } /** * 测试删除黑名单 */ public void testDelete() { BlackNumberDao.getInstance(getContext()).delete("13812345601"); } /** * 测试更新黑名单 */ public void testUpdate() { BlackNumberDao.getInstance(getContext()).update("13812345600", 2); } /** * 测试查找黑名单 */ public void testFind() { boolean find = BlackNumberDao.getInstance(getContext()).find( "13812345600"); assertEquals(true, find); } /** * 测试查找黑名单拦截模式 */ public void testFindMode() { int mode = BlackNumberDao.getInstance(getContext()).findMode( "13812345600"); System.out.println("拦截模式:" + mode); } }
- 使用命令行查看数据库文件
1. 运行adb shell进入linux环境 2. 切换至data/data/包名/databases 3. 运行sqlite3 *.db,进入数据库 4. 编写sql语句,进行相关操作.记得加分号(;)结束 5. .quit退出sqlite,切换到adb shell
- 使用命令行查看数据库文件
- 单元测试增加随机数据, Activity进行展示
-
listView优化
- 使用traceview工具可以计算方法运行时间
1. 在onCreate中调用Debug.startMethodTracing();开始记录时间 2. 在onDestroy中调用Debug.stopMethodTracing();停止记录时间 3. 运行app, 进入该页面,滑动listview列表 4. 系统会自动在sdcard根目录生成dmtrace.trace文件 5. 进入命令行,在sdk/tools下调用traceview命令: traceview dmtrace.trace 6. 进入可视化界面,搜索getview方法,查看该方法的平均调用时间,优化前和优化后的时间进行对比
- 在不优化的情况下,运行代码,查看logcat系统日志输出, 可以发现dalvic虚拟机剩余内存越来越小. 此时可以使用traceview进一步计算出getView方法的执行时间
-
介绍convertView的重用机制
-
介绍ViewHolder的使用方法
//使用static修饰内部类,系统只加载一份字节码文件,节省内存 static class ViewHolder { public TextView tvNumber; public TextView tvMode; }
- 使用convertView和ViewHolder进行优化之后,重新使用traceview计算getView的执行时间,进行对比
-
最终优化结果
@Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; ViewHolder holder = null; if (convertView == null) { view = View.inflate(BlackNumberActivity.this, R.layout.list_black_number_item, null); System.out.println("listview创建"); // viewHolder类似一个容器,可以保存findViewById获得的view对象 holder = new ViewHolder(); holder.tvNumber = (TextView) view.findViewById(R.id.tv_number); holder.tvMode = (TextView) view.findViewById(R.id.tv_mode); // 将viewHolder设置给view对象,保存起来 view.setTag(holder); } else { view = convertView; holder = (ViewHolder) view.getTag();// 从view对象中得到之前设置好的viewHolder System.out.println("listview重用了"); } BlackNumberInfo info = mBlackNumberList.get(position); holder.tvNumber.setText(info.number); switch (info.mode) { case 1: holder.tvMode.setText("拦截电话"); break; case 2: holder.tvMode.setText("拦截短信"); break; case 3: holder.tvMode.setText("拦截电话+短信"); break; } return view; } static class ViewHolder { public TextView tvNumber; public TextView tvMode; }
- 使用traceview工具可以计算方法运行时间
- 启动子线程在数据库读取数据
当数据量比较大时,读取数据比较耗时,为了避免ANR,最好将该逻辑放在子线程中进行, 为了模拟数据量大时访问比较慢的情况,可以让线程休眠1-2秒后再加载数据
- 加载中的进度条展示
- 数据分批加载
分批加载优势:避免一次性加载过多内容, 节省时间和流量 sql语句: select * from blacknumber limit 20 offset 0, 表示其实位置是0,加载条数为20, 等同于limit 0,20 /** * 分页查找黑名单列表 * * @return */ public ArrayList<BlackNumberInfo> findPart(int startIndex) { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db.rawQuery( "select number,mode from blacknumber order by _id desc limit 20 offset ?", new String[] { String.valueOf(startIndex) }); ArrayList<BlackNumberInfo> list = new ArrayList<BlackNumberDao.BlackNumberInfo>(); while (cursor.moveToNext()) { String number = cursor.getString(0); int mode = cursor.getInt(1); BlackNumberInfo info = new BlackNumberInfo(); info.number = number; info.mode = mode; list.add(info); } cursor.close(); db.close(); return list; } /** * 获取黑名单数量 * * @return */ public int getTotalCount() { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db.rawQuery("select count(*) from blacknumber", null); int count = 0; if (cursor.moveToNext()) { count = cursor.getInt(0); } cursor.close(); db.close(); return count; } -------------------------------------- //监听listview的滑动事件 lvList.setOnScrollListener(new OnScrollListener() { // 滑动状态发生变化 // 1.静止->滚动 2.滚动->静止 3.惯性滑动 @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == SCROLL_STATE_IDLE) { int totalCount = BlackNumberDao.getInstance( BlackNumberActivity.this).getTotalCount(); //判断是否已经到达最后一页 if (mStartIndex >= totalCount) { Toast.makeText(BlackNumberActivity.this, "没有更多数据了", Toast.LENGTH_SHORT).show(); return; } //获取当前listview显示的最后一个item的位置 int lastVisiblePosition = lvList.getLastVisiblePosition(); //判断是否应该加载下一页 if (lastVisiblePosition >= mBlackNumberList.size() - 1 && !isLoading) { Toast.makeText(BlackNumberActivity.this, "加载更多数据...", Toast.LENGTH_SHORT).show(); System.out.println("加载更多数据..."); initData(); } } } ----------------------------------- //加载数据 private void initData() { pbLoading.setVisibility(View.VISIBLE);//显示进度条 isLoading = true; new Thread() { @Override public void run() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 第一页数据 if (mBlackNumberList == null) { mBlackNumberList = BlackNumberDao.getInstance( BlackNumberActivity.this).findPart(mStartIndex); } else { mBlackNumberList.addAll(BlackNumberDao.getInstance( BlackNumberActivity.this).findPart(mStartIndex)); } mHandler.sendEmptyMessage(0); } }.start(); } ----------------------------------- private int mStartIndex;//下一页的起始位置 private boolean isLoading;// 表示是否正在加载 private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { pbLoading.setVisibility(View.GONE);//隐藏进度条 // 第一页数据 if (mAdapter == null) { mAdapter = new BlackNumberAdapter(); lvList.setAdapter(mAdapter); } else { mAdapter.notifyDataSetChanged();//刷新adapter } mStartIndex = mBlackNumberList.size(); isLoading = false; }; };
- 添加黑名单
/** * 添加黑名单 * * @param view */ public void addBlackNumber(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = View.inflate(this, R.layout.dialog_add_black_number, null); final AlertDialog dialog = builder.create(); dialog.setView(view, 0, 0, 0, 0); final EditText etBlackNumber = (EditText) view .findViewById(R.id.et_black_number); final RadioGroup rgMode = (RadioGroup) view.findViewById(R.id.rg_mode); Button btnOK = (Button) view.findViewById(R.id.btn_ok); Button btnCancel = (Button) view.findViewById(R.id.btn_cancel); btnOK.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String number = etBlackNumber.getText().toString().trim(); if (!TextUtils.isEmpty(number)) { int checkedRadioButtonId = rgMode.getCheckedRadioButtonId(); int mode = 1; // 根据当前选中的RadioButtonId来判断是哪种拦截模式 switch (checkedRadioButtonId) { case R.id.rb_call: mode = 1; break; case R.id.rb_sms: mode = 2; break; case R.id.rb_all: mode = 3; break; default: break; } // 保存数据库 BlackNumberDao.getInstance(getApplicationContext()).add( number, mode); // 向列表第一个位置增加黑名单对象,并刷新listview //注意: 分页查询时需要逆序排列,保证后添加的最新数据展示在最前面 BlackNumberInfo info = new BlackNumberInfo(); info.number = number; info.mode = mode; mBlackNumberList.add(0, info); mAdapter.notifyDataSetChanged(); dialog.dismiss(); } else { Toast.makeText(getApplicationContext(), "输入内容不能为空!", Toast.LENGTH_SHORT).show(); } } }); btnCancel.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { dialog.dismiss(); } }); dialog.show(); }
- 删除黑名单
- 事件传递机制简介
- 黑名单删除弹窗提示(留给学生做)
holder.ivDelete.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //从数据库中删除 BlackNumberDao.getInstance(getApplicationContext()).delete( info.number); //从内存列表中删除并刷新listview mBlackNumberList.remove(info); mAdapter.notifyDataSetChanged(); } });
- 创建黑名单拦截服务
- 拦截短信逻辑实现
逻辑类似手机防盗页面拦截特殊短信指令的代码, 只不过该广播是动态注册,不是静态注册. 动态注册的好处是可以随服务的开启或关闭来决定是否监听广播,而且在同等优先级的前提下,动态注册的广播比静态注册的更先接收到广播(可以通过打印日志进行验证)
- 拦截短信逻辑实现
- 设置页面增加黑名单拦截开关
通过此开关来开启和关闭服务, 逻辑类似来电归属地显示的开关
- 短信拦截优化
- 通过关键词智能拦截(介绍)
- 金山卫士智能拦截简介
- 金山卫士关键词数据库
查看第四天资料,金山卫士apk解压文件,assets目录下找firewall_sys_rules.db, 该数据制定了短信和来电的拦截规则 短信拦截规则: 根据关键词对短信内容进行过滤. 比如fapiao //对短信内容进行关键词过滤 String messageBody = msg.getMessageBody(); if (messageBody != null && messageBody.contains("fapiao")) { abortBroadcast(); }
- 分词(介绍)
单纯依靠关键词进行过滤有时会出现一些问题, 比如: laogong, nikan,wode toufapiaobupiaoliang.... 所以有时候会对每一句话进行分词处理,比如可以将上述语句先拆分成不同的词语:laogong,nikan,wode,toufa,piaobupiaoliang, 然后在这些词汇中对关键词再进行过滤 lucene分词检索框架
- 保存拦截到的短信(介绍)
一般拦截到短信后都会在数据库中保存,原因有以下几点 1. 便于再次查看; 2. 有些人喜欢看垃圾短信; 3. 防止拦截误判
- 短信拦截的兼容性处理
4.4以上系统手机对短信权限进一步限制,导致无法拦截短信,可以通过监听短信数据库的变化,及时删除最新入库的垃圾短信来实现短信拦截的目的. 为了避免误删旧的短信,需要和短信广播结合起来使用
- 通过关键词智能拦截(介绍)
- 来电拦截
1. 挂断电话的API早期版本endCall()是可以使用的,现在不可以用了;但本身挂断电话这个功能是存在的 2. 读getSystemService()源代码,但发现底层是抽象方法 3. 如何查看真面目?到内存中去看,运行起来看,打断点。例如 mTM = (TelephonyManager) this.getSystemService(TELEPHONY_SERVICE);//查看this的信息->看他里面的mBase->ContextImpl.java 4. 搜索:ContextImpl.java,看看源代码,发现ContextImpl继承了Context 5. 搜索getSystemService,查看底层代码实现 6. 通过查看源码发现,很多系统级的服务都是在静态代码块中一开始就创建好的,为了验证我们的想法,可以重启模拟器,根据系统日志查看服务的启动情况 7. 我们发现,很多服务都是获取远程服务的代理对象IBinder,再调用里面的方法的.例如: IBinder b = ServiceManager.getService(ALARM_SERVICE); IAlarmManager service = IAlarmManager.Stub.asInterface(b); return new AlarmManager(service); 8. 于是我们跟踪TelephoneyManager,查看它的对象到底是如何创建的.我们跟踪到了这样一个方法: private ITelephony getITelephony() { return ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)); } 改方法返回一个ITelephony对象, 查看ITelephony对象的方法,发现有endCall方法 9. 于是我们将获取ITelephony的代码拷贝到自己的项目中,发现无法导包,因为根本有没有ServiceManager这个类,但我们知道它肯定存在,因为TelephonyManager就引用了该类,只不过android系统隐藏了这个类, 10. 为了调用隐藏类的方法,我们想到了反射
- 通过反射获取endCall方法
/** * 挂断电话 * 注意加权限: <uses-permission * android:name="android.permission.CALL_PHONE"/> */ public void endCall() { try { // 获取ServiceManager Class clazz = BlackNumberService.class.getClassLoader().loadClass( "android.os.ServiceManager"); Method method = clazz.getDeclaredMethod("getService", String.class);// 获取方法getService IBinder binder = (IBinder) method.invoke(null, Context.TELEPHONY_SERVICE);// 方法时静态的,不需要传递对象进去 ITelephony telephony = ITelephony.Stub.asInterface(binder);// 获取ITelephony对象,前提是要先配置好aidl文件 telephony.endCall();//挂断电话 } catch (Exception e) { e.printStackTrace(); } } 注意加权限: <uses-permission android:name="android.permission.CALL_PHONE"/>
- 清除来电记录
代码挂断电话后,被挂断的号码仍然会进入通话记录中, 我们需要将这种记录删除. 查看数据库contacts2中的表calls /** * 删除通话记录 */ private void deleteCallLog(String number) { getContentResolver().delete(Uri.parse("content://call_log/calls"), "number=?", new String[] {number}); } 注意加权限: <uses-permission android:name="android.permission.READ_CALL_LOG"/> <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
- 通过内容观察者,解决通话记录删除失败的问题
系统在往通话记录的数据库中插入数据时是异步逻辑,所以当数据库还没来得及添加电话日志时,我们就执行了删除日志的操作,从而导致删除失败,为了避免这个问题,可以监听数据库变化,当数据库发生变化后,我们才执行删除操作,从而解决这个问题 /** * 内容观察者 * @author Kevin * */ class MyContentObserver extends ContentObserver { private String incomingNumber; public MyContentObserver(Handler handler, String incomingNumber) { super(handler); this.incomingNumber = incomingNumber; } /** * 当数据库发生变化时,回调此方法 */ @Override public void onChange(boolean selfChange) { System.out.println("call log changed..."); //删除日志 deleteCallLog(incomingNumber); //删除完日志后,注销内容观察者 getContentResolver().unregisterContentObserver(mObserver); } } ------------------------------ //监听到来电时,注册内容观察者 mObserver = new MyContentObserver(new Handler(), incomingNumber); //注册内容观察者 getContentResolver().registerContentObserver( Uri.parse("content://call_log/calls"), true, mObserver); ------------------------------ 注意: 补充Android2.3模拟器上需要多加权限 <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
- 通过反射获取endCall方法
Day07
- 短信备份
- 查看短信数据库
data/data/com.android.provider.telephony/databases/mmssms.db address 短信收件人发件人地址 date 短信接收的时间 type 1 发进来短信 2 发出去短信 read 1已读短信 0 未读短信 body 短信内容
- 读取短信数据库内容
查看系统源码,找到uri地址:packages\provider\platform_packages_providers_telephonyprovider-master Uri uri = Uri.parse("content://sms/");// 所有短信 Cursor cursor = ctx.getContentResolver().query(uri, new String[] { "address", "date", "type", "body" }, null, null, null); 遍历cursor,获取短信信息 注意权限: <uses-permission android:name="android.permission.READ_SMS"/> <uses-permission android:name="android.permission.WRITE_SMS"/>
- 将短信内容序列化为xml文件
sms.xml <?xml version="1.0" encoding="utf-8"?> <smss> <sms> <address>5556</address> <date>10499949433</date> <type>1</type> <body>wos shi haoren</body> </sms> <sms> <address>13512345678</address> <date>1049994889433</date> <type>2</type> <body>hell world hei ma</body> </sms> </smss> ------------------------------ XmlSerializer serializer = Xml.newSerializer();// 初始化xml序列化工具 serializer.setOutput(new FileOutputStream(output), "utf-8");//设置输出流 /* * startDocument(String encoding, Boolean standalone)encoding代表编码方式 * standalone 用来表示该文件是否呼叫其它外部的约束文件。 若值是 ”yes” 表示没有呼叫外部规则文件,若值是 ”no” * 则表示有呼叫外部规则文件。默认值是 “yes”。 * <?xml version="1.0" encoding="utf-8" standalone=true ?> * * 参数2传null时,不会生成standalone=true/false的语句 */ serializer.startDocument("utf-8", null);// 生成xml顶栏描述语句<?xml // version="1.0" // encoding="utf-8"?> serializer.startTag(null, "smss");//起始标签 serializer.text(body);// 设置内容 serializer.endTag(null, "smss");//结束标签 serializer.endDocument();//结束xml文档 ------------------------------ AToolsActivity.java /** * 短信备份 */ public void smsBackup(View view) { if (Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState())) { try { SmsUtils.smsBackup(this, new File(Environment.getExternalStorageDirectory(), "sms.xml")); Toast.makeText(this, "备份成功!", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, "备份失败!", Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(this, "没有检测到sdcard!", Toast.LENGTH_SHORT).show(); } }
- 异步备份短信,并显示进度条
mProgressDialog = new ProgressDialog(this); mProgressDialog.setMessage("正在备份短信..."); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);//设置显示风格,此风格将展示一个进度条 mProgressDialog.show(); 将ProgressDialog的引用传递给工具类,在工具类中更新进度 SmsUtils.smsBackup(AToolsActivity.this, new File( Environment.getExternalStorageDirectory(), "sms.xml"), mProgressDialog); -------------------------------- //短信工具类中更新进度条的逻辑 progressDialog.setMax(cursor.getCount());// 设置进度条最大值 Thread.sleep(500);//为了方便看效果,故意延时1秒钟 progress++; progressDialog.setProgress(progress);//更新进度 -------------------------------- 模拟需求变动的情况 1. A负责短信备份界面, B负责短信工具类 2. 将ProgressDialog改动为ProgressBar, 需要A通知B改动 3. 又讲ProgressBar改回ProgressDialog, 需要A通知B改动 4. 既有ProgressBar,又要求有ProgressDialog, 需要A通知B改动 问题: B除了负责底层业务逻辑之外,额外还需要帮A处理界面逻辑,如果实现A和B的解耦?
- 使用回调接口通知进度,优化代码,实现解耦
/** * 短信备份回调接口 * @author Kevin * */ public interface SmsBackupCallback { /** * 备份之前获取短信总数 * @param total */ public void preSmsBackup(int total); /** * 备份过程中实时获取备份进度 * @param progress */ public void onSmsBackup(int progress); } ---------------------------- SmsUtils.smsBackup(AToolsActivity.this, new File( Environment.getExternalStorageDirectory(), "sms.xml"), new SmsBackupCallback() { @Override public void preSmsBackup(int total) { mProgressDialog.setMax(total); } @Override public void onSmsBackup(int progress) { mProgressDialog.setProgress(progress); } });
- 查看短信数据库
- 短信还原(介绍)
-
应用管理器(AppManagerActivity)
- 介绍金山卫士的应用管理器
- 参考金山卫士,编写布局文件
- 计算内置存储空间和sdcard剩余空间
/** * 获取剩余空间 * * @param path * @return */ private String getAvailSpace(String path) { StatFs stat = new StatFs(path); // Integer.MAX_VALUE; // int最大只能表示到2G, 在一些高端手机上不足够接收大于2G的容量,所以可以用long来接收, 相乘的结果仍是long类型 long blocks = stat.getAvailableBlocks();// 获取可用的存储块个数 long blockSize = stat.getBlockSize();// 获取每一块的大小 return Formatter.formatFileSize(this, blocks * blockSize);// 将字节转化为带有容量单位的字符串 } //获取内存的地址 Environment.getDataDirectory().getAbsolutePath() //获取sdcard的地址 Environment.getExternalStorageDirectory().getAbsolutePath()
- 获取已安装的应用列表
/** * 应用信息封装 * * @author Kevin * */ public class AppInfo { public String name;// 名称 public String packageName;// 包名 public Drawable icon;// 图标 public boolean isUserApp;// 是否是用户程序 public boolean isRom;// 是否安装在内置存储器中 @Override public String toString() { return "AppInfo [name=" + name + ", packageName=" + packageName + "]"; } } ------------------------------- AppInfoProvider.java /** * 获取已安装的应用信息 * @param ctx */ public static ArrayList<AppInfo> getAppInfos(Context ctx) { ArrayList<AppInfo> infoList = new ArrayList<AppInfo>(); PackageManager pm = ctx.getPackageManager(); List<PackageInfo> packages = pm.getInstalledPackages(0);// 获取已经安装的所有包 for (PackageInfo packageInfo : packages) { AppInfo info = new AppInfo(); String packageName = packageInfo.packageName;// 获取包名 Drawable icon = packageInfo.applicationInfo.loadIcon(pm);// 获取图标 String name = packageInfo.applicationInfo.loadLabel(pm).toString();// 获取名称 info.packageName = packageName; info.icon = icon; info.name = name; infoList.add(info); } return infoList; } 进行单元测试,打印应用列表
- Android的应用程序安装位置
pc电脑默认安装在C:\Program Files Android 的应用安装在哪里呢,如果是用户程序,安装在data/app/目录下, 系统带应用安装在system/app/目录下 安装Android软件 做两件事 A:把APK拷贝到data/app/目录下 B:把安装包信息写到data/system/目录下两个文件packages.list 和 packages 安装包信息在data/system/ Packages.list 里面的0 表示系统应用 1 表示用户应用 Packages.xml是存放应用的一些权限信息的;
- 判断是系统应用还是用户应用
- 解释标识左移几位的效果public static final int FLAG_SYSTEM = 1<<0;
- 不同标识可以加起来一起使用, 加起来的结果和特定标识进行与运算,通过计算结果可以知道具不具备该特定标识的相关功能, 这种方式叫做状态机
- 玩游戏距离: 喝药水,加功能(加血,加攻击力,加防御,加魔法值),可以通过状态机来表示该药水具备哪些特性
- 实际开发的机顶盒举例:{“cctv1″:true,”cctv2″:false,”cctv3”:true}->{“flag”:101}, 可以节省流量
if ((flags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) { info.isUserApp = false;// 系统应用 } else { info.isUserApp = true;//用户应用 } if ((flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) == ApplicationInfo.FLAG_EXTERNAL_STORAGE) { info.isRom = false;// 安装位置是sdcard } else { info.isRom = true;//安装位置是内置存储器 }
- ListView列表展现
- 仿照金山卫士编写item布局
- 异步加载应用列表数据,并显示进度条
加载应用列表有时会比较耗时,最好放在子线程执行. 加载期间显示加载中的布局 <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@+id/lv_list" android:layout_width="match_parent" android:layout_height="match_parent" > </ListView> <LinearLayout android:id="@+id/ll_loading" android:visibility="gone" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" android:paddingBottom="50dp" android:orientation="vertical" > <ProgressBar android:id="@+id/progressBar1" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="正在加载应用列表..." /> </LinearLayout> <TextView android:id="@+id/tv_head" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#FF888888" android:textColor="#fff" android:text="用户程序(5)" />
-
清单文件中注册安装位置
manifest根标签具有这样的属性,用来指定apk的安装位置, 缺省值是手机内存 android:installLocation="auto", 可以修改为auto, 表示优先使用手机内存, 如果内存不够,再使用sdcard. 不建议强制安装在sdcard,因为某些手机没有sdcard,会导致安装失败.设置完成后,就可以在系统应用管理中,移动apk的安装位置了.
- 复杂ListView的展现方式
核心思想: 重写BaseAdapter自带的getItemViewType方法来返回item类型,重写getViewTypeCount方法返回类型个数 //应用信息适配器 class AppInfoAdapter extends BaseAdapter { /** * 返回总数量,包括两个标题栏 */ @Override public int getCount() { return 1 + mUserAppList.size() + 1 + mSystemAppList.size(); } /** * 返回当前的对象 */ @Override public AppInfo getItem(int position) { if (position == 0 || position == 1 + mUserAppList.size()) {//判断是否是标题栏 return null; } AppInfo appInfo; if (position < mUserAppList.size() + 1) {//判断是否是用户应用 appInfo = mUserAppList.get(position - 1); } else {//系统应用 appInfo = mSystemAppList .get(position - mUserAppList.size() - 2); } return appInfo; } @Override public long getItemId(int position) { return position; } /** * 返回当前view的类型 */ @Override public int getItemViewType(int position) { if (position == 0 || position == 1 + mUserAppList.size()) { return 1;// 标题栏 } else { return 0;// 应用信息 } } /** * 返回当前item的类型个数 */ @Override public int getViewTypeCount() { return 2; } @Override public View getView(int position, View convertView, ViewGroup parent) { int type = getItemViewType(position);//获取item的类型 if (convertView == null) {//初始化convertView switch (type) { case 1://标题 convertView = new TextView(AppManagerActivity.this); ((TextView) convertView).setTextColor(Color.WHITE); ((TextView) convertView).setBackgroundColor(Color.GRAY); break; case 0://应用信息 ViewHolder holder; convertView = View.inflate(AppManagerActivity.this, R.layout.list_appinfo_item, null); holder = new ViewHolder(); holder.ivIcon = (ImageView) convertView .findViewById(R.id.iv_icon); holder.tvName = (TextView) convertView .findViewById(R.id.tv_name); holder.tvLocation = (TextView) convertView .findViewById(R.id.tv_location); convertView.setTag(holder); break; default: break; } } //根据类型来更新view的显示内容 switch (type) { case 1: if (position == 0) { ((TextView) convertView).setText("用户程序(" + mUserAppList.size() + ")"); } else { ((TextView) convertView).setText("系统程序(" + mUserAppList.size() + ")"); } break; case 0: ViewHolder holder = (ViewHolder) convertView.getTag(); AppInfo appInfo = getItem(position); holder.ivIcon.setImageDrawable(appInfo.icon); holder.tvName.setText(appInfo.name); if (appInfo.isRom) { holder.tvLocation.setText("手机内存"); } else { holder.tvLocation.setText("外置存储卡"); } break; default: break; } return convertView; } }
- PopupWindow使用
专门写一个Demo,用于PopupWindow的演示 /** * 显示弹窗 * * @param view */ public void showPopupWindow(View view) { TextView contentView = new TextView(this); contentView.setText("我是弹窗哦!"); contentView.setTextColor(Color.RED); PopupWindow popup = new PopupWindow(contentView, 100, 100, true);//设置尺寸及获取焦点 popup.setBackgroundDrawable(new ColorDrawable(Color.BLUE));//设置背景颜色 // popup.showAtLocation(rlRoot, Gravity.LEFT + Gravity.TOP, 0, // 0);//显示在屏幕的位置 popup.showAsDropDown(btnPop, 0, 0);// 显示在某个控件的正下方 }
- 将PopupWindow应用到项目当中
//listview监听 lvList.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mCurrentAppInfo = mAdapter.getItem(position); System.out.println(mCurrentAppInfo.name + "被点击!"); showPopupWindow(view); } }); ----------------------------------- /** * 显示弹窗 */ private void showPopupWindow(View view) { View contentView = View.inflate(this, R.layout.popup_appinfo, null); TextView tvUninstall = (TextView) contentView .findViewById(R.id.tv_uninstall); TextView tvLaunch = (TextView) contentView.findViewById(R.id.tv_launch); TextView tvShare = (TextView) contentView.findViewById(R.id.tv_share); //设置监听事件 tvUninstall.setOnClickListener(this); tvLaunch.setOnClickListener(this); tvShare.setOnClickListener(this); //初始化PopupWindow PopupWindow popup = new PopupWindow(contentView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true); popup.setBackgroundDrawable(new ColorDrawable());// 必须设置背景,否则无法返回 popup.showAsDropDown(view, 50, -view.getHeight());// 显示弹窗 //渐变动画 AlphaAnimation alpha = new AlphaAnimation(0, 1); alpha.setDuration(500); alpha.setFillAfter(true); //缩放动画 ScaleAnimation scale = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0.5f); scale.setDuration(500); scale.setFillAfter(true); //动画集合 AnimationSet set = new AnimationSet(false); set.addAnimation(alpha); set.addAnimation(scale); //运行动画 contentView.startAnimation(set); } ----------------------------------- @Override public void onClick(View v) { if (mCurrentAppInfo == null) { return; } switch (v.getId()) { case R.id.tv_uninstall: System.out.println("卸载" + mCurrentAppInfo.name); break; case R.id.tv_launch: System.out.println("启动" + mCurrentAppInfo.name); break; case R.id.tv_share: System.out.println("分享" + mCurrentAppInfo.name); break; default: break; } }
- 利用PopupWindow的背景图片,演示图片放在不同的drawable目录后的效果(难点)
当前手机是mdpi的屏幕, 如果把图片放在drawable目录下, 图片多大就展示多大. 如果把图片放在drawable-h的目录下, 图片会在mdpi的屏幕上压缩; 如果放在drawable-l的目录下, 图片会在mdpi的屏幕上放大; 为了避免图片放在不同目录下产生的大小变化, 通常可以固定图片的宽高, 但固定宽高后可能造成图片由于压缩而发虚的情况. 由于市面上手机基本在hdpi以上,所以我们尽量可以将图片放在drawable-h的目录下,从而既能保证h以上显示正常,也能保证h屏没太大问题。 如果我们都放在了xh的目录下,就有可能导致h屏手机图片被压缩
- 卸载,启动和分享的逻辑
/** * 卸载 */ private void uninstall() { if (mCurrentAppInfo.isUserApp) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setData(Uri.parse("package:" + mCurrentAppInfo.packageName)); startActivityForResult(intent, 0); } else { Toast.makeText(this, "无法卸载系统程序!", Toast.LENGTH_SHORT).show(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // 卸载成功后重新加载应用列表 // 此处不必判断resultCode是否是RESULT_OK, 因为4.1+系统即使卸载成功也始终返回RESULT_CANCEL loadAppInfos(); super.onActivityResult(requestCode, resultCode, data); } ------------------------------------ /** * 启动App */ private void launchApp() { try { PackageManager pm = this.getPackageManager(); Intent intent = pm .getLaunchIntentForPackage(mCurrentAppInfo.packageName);// 获取应用入口的Intent startActivity(intent);// 启动应用 } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, "无法启动该应用!", Toast.LENGTH_SHORT).show(); } } ------------------------------------ /** * 分享 此方法会呼起系统中所有支持文本分享的app列表 */ private void shareApp() { Intent intent = new Intent(Intent.ACTION_SEND); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, "分享给你一个很好的应用哦! 下载地址: https://play.google.com/apps/details?id=" + mCurrentAppInfo.packageName); startActivity(intent); }
- ListView分类栏常驻效果
//原理: 写一个TextView常驻在ListView顶栏, 样式和item中分类栏的样式完全一样. 监听ListView的滑动事件,动态修改TextView的内容 //设置listview的滑动监听 lvList.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { System.out.println("onScroll:" + firstVisibleItem); if (mUserAppList != null && mSystemAppList != null) { if (firstVisibleItem <= mUserAppList.size()) { tvListHead.setText("用户应用(" + mUserAppList.size() + ")"); } else { tvListHead.setText("系统应用(" + mSystemAppList.size() + ")"); } } } });
Day08
- 常用号码查询
- 在高级工具中增加常用号码查询的入口
CommonNumberActivity
- ExpandableListView的使用
1. 实现一个简单的页面, 填充假数据进行演示 BaseExpandableListAdapter.java /** * 获取组的个数 */ @Override public int getGroupCount() { return 8; } @Override public int getChildrenCount(int groupPosition) { return groupPosition+1; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { TextView tvGroup = new TextView(getApplicationContext()); tvGroup.setTextColor(Color.RED); tvGroup.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); tvGroup.setText(" 第" + groupPosition + "组"); return tvGroup; } @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { TextView tvChild = new TextView(getApplicationContext()); tvChild.setTextColor(Color.BLACK); tvChild.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); tvGroup.setText("第" + groupPosition + "组" + ";第" + childPosition + "项"); return tvChild; } -------------------------------------- 2. 使用金山的常用号码数据库 CommonNumberDao.java public class CommonNumberDao { public static final String PATH = "data/data/com.itheima.mobilesafeteach/files/commonnum.db"; /** * 获取所有常用号码分组 * * @return */ public static ArrayList<CommonNumberGroup> getCommonNumberGroups() { SQLiteDatabase db = SQLiteDatabase.openDatabase(PATH, null, SQLiteDatabase.OPEN_READONLY); Cursor cursor = db.query("classlist", new String[] { "name", "idx" }, null, null, null, null, null); ArrayList<CommonNumberGroup> list = new ArrayList<CommonNumberDao.CommonNumberGroup>(); while (cursor.moveToNext()) { CommonNumberGroup group = new CommonNumberGroup(); String name = cursor.getString(0); String idx = cursor.getString(1); group.name = name; group.idx = idx; group.childs = getCommonNumberChilds(db, idx); list.add(group); } cursor.close(); db.close(); return list; } /** * 获取某个分组下的所有电话 * * @param id * 分组的id */ public static ArrayList<CommonNumberChild> getCommonNumberChilds( SQLiteDatabase db, String id) { Cursor cursor = db .query("table" + id, new String[] { "number", "name" }, null, null, null, null, null); ArrayList<CommonNumberChild> list = new ArrayList<CommonNumberDao.CommonNumberChild>(); while (cursor.moveToNext()) { CommonNumberChild child = new CommonNumberChild(); String number = cursor.getString(0); String name = cursor.getString(1); child.name = name; child.number = number; list.add(child); } cursor.close(); return list; } /** * 常用号码分组 * * @author Kevin * */ public static class CommonNumberGroup { public String name; public String idx; public ArrayList<CommonNumberChild> childs; } /** * 常用号码 * * @author Kevin * */ public static class CommonNumberChild { public String name; public String number; } } --------------------------- 3. 初始化数据, 填充真实数据 /** * 初始化号码数据 */ private void initData() { mNumberGroups = CommonNumberDao.getCommonNumberGroups(); } --------------------------- 4. 设置item的点击事件, 跳转拨打电话页面 /** * 表示孩子是否可以点击选中 */ @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } //child项点击监听 elvList.setOnChildClickListener(new OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { CommonNumberChild child = mAdapter.getChild(groupPosition, childPosition); // 跳转拨打电话的界面 // <uses-permission android:name="android.permission.CALL_PHONE" // /> // 允许拨打电话权限 Intent intent = new Intent(Intent.ACTION_DIAL, Uri .parse("tel://" + child.number)); startActivity(intent); return false; } }); ----------------------------- 5. ListView的简单优化 @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { TextView tvGroup; if (convertView == null) { tvGroup = new TextView(getApplicationContext()); tvGroup.setTextColor(Color.RED); tvGroup.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); // tvGroup.setText(" 第" + groupPosition + "组"); } else { tvGroup = (TextView) convertView; } tvGroup.setText(" " + getGroup(groupPosition).name); return tvGroup; } @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { ...... }
- 在高级工具中增加常用号码查询的入口
- 创建快捷键
- 演示金山卫士创建桌面快捷方式进行快速拨号的功能
-
快捷键特点
1. 名称 2. 图标 3. 动作 4. 桌面 结束PC的桌面进程,或者删除手机的桌面apk 1. 打开任务管理器,结束explorer.exe进程, 发现桌面消失. 选择任务管理器->文件->新建任务->explorer.exe, 重新启动桌面 2. 打开adb shell, 进入手机的/system/app/目录, 尝试删除Launcher2.apk, 删除失败, 因为文件系统只读 运行命令,重新挂载system目录: mount -o remount ,rw /system 然后再尝试删除,就可以了. 桌面被删除后,手机桌面展示黑屏.
- 一键呼叫
查看Launcher源码的清单文件 //需要权限: <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/> public void click(View view) { Intent intent = new Intent( "com.android.launcher.action.INSTALL_SHORTCUT"); // 应用名称 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, "立即报警"); // 应用图标 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapFactory .decodeResource(getResources(), R.drawable.home_apps)); // 应用动作 Intent actionIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel://110")); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent); //发送广播 sendBroadcast(intent); } 注意: 不需要额外添加打电话的权限, 因为Launcher的清单文件中已经配置了打电话的权限, 而桌面快捷方式属于Launcher一部分,所以也就具备了打电话的权限.
- 将创建快捷方式移植到项目当中
// 创建快捷方式 // 需要权限: <uses-permission // android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/> private void createShortcut() { SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE); boolean created = sp.getBoolean("is_shortcut_created", false); if (!created) {// 如果没创建,才开始创建,否则会创建多个快捷方式 Intent intent = new Intent( "com.android.launcher.action.INSTALL_SHORTCUT"); // 应用名称 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, "黑马卫士"); // 应用图标 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, BitmapFactory .decodeResource(getResources(), R.drawable.home_apps)); // 应用动作 Intent actionIntent = new Intent(); actionIntent.setAction("com.itheima.mobilesafe.home");//设置action, 需要在清单文件中配置 //actionIntent.setClass(this, HomeActivity.class); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent); // 发送广播 sendBroadcast(intent); sp.edit().putBoolean("is_shortcut_created", true).commit(); } }
Day09
- 进程管理(TaskManagerActivity)
TaskInfoProvider
- 获取运行中的内存数
- 获取剩余内存
- 获取总内存
/** * 获取正在运行的进程数 */ public static int getRunningTaskNum(Context ctx) { ActivityManager am = (ActivityManager) ctx .getSystemService(Context.ACTIVITY_SERVICE); return am.getRunningAppProcesses().size(); } /** * 获取剩余内存 */ public static long getAvailMemory(Context ctx) { ActivityManager am = (ActivityManager) ctx .getSystemService(Context.ACTIVITY_SERVICE); MemoryInfo outInfo = new MemoryInfo(); am.getMemoryInfo(outInfo); return outInfo.availMem; } /** * 获取总内存(API16以下会崩溃) */ public static long getTotalMemory(Context ctx) { ActivityManager am = (ActivityManager) ctx .getSystemService(Context.ACTIVITY_SERVICE); MemoryInfo outInfo = new MemoryInfo(); am.getMemoryInfo(outInfo); return outInfo.totalMem; }
- 获取总内存低版本兼容
原理: 读取系统根目录/proc/meminfo的系统配置文件, 获取总内存大小 扩展: 读取/proc/cpuinfo, arm模拟器和x86模拟器性能对比 常用命令: adb devices(查看已连接设备), adb -s emulator-5554 shell(进入指定设备) 注意: meminfo中MemFree + Cached(缓存内存) = 剩余总内存, 和api获取的剩余内存基本一致 /** * 获取总内存 */ public static long getTotalMemory(Context ctx) { BufferedReader br = null; FileReader reader = null; // 处理兼容问题 try { reader = new FileReader("/proc/meminfo");// 读取系统文件 br = new BufferedReader(reader); char[] readLine = br.readLine().toCharArray();// 获取第一行内容 StringBuffer sb = new StringBuffer(); for (char c : readLine) { if (c >= '0' && c <= '9') {// 判断是否是数字, 一定要用字符形式的数字,比如'0', 而不是0 sb.append(c); } } return Integer.parseInt(sb.toString()) * 1024;// 转成整数后,乘以1024转成字节数返回 } catch (Exception e) { e.printStackTrace(); } finally { try { br.close(); reader.close(); } catch (Exception e) { } } return 0; }
- 获取进程信息列表并展示
- 获取数据
public class TaskInfo { public String name;// 名称 public String packageName;// 包名 public Drawable icon;// 图标 public long memory;// 占用内存 public boolean isUserTask;// 标记是否是用户进程 } /** * 获取所有进程信息 */ public static ArrayList<TaskInfo> getTaskInfos(Context ctx) { ActivityManager am = (ActivityManager) ctx .getSystemService(Context.ACTIVITY_SERVICE); PackageManager pm = ctx.getPackageManager(); // 获取所有正在运行的进程 List<RunningAppProcessInfo> appProcesses = am.getRunningAppProcesses(); ArrayList<TaskInfo> taskInfos = new ArrayList<TaskInfo>(); for (RunningAppProcessInfo runningAppProcessInfo : appProcesses) { TaskInfo taskInfo = new TaskInfo(); int pid = runningAppProcessInfo.pid; // 获取内存信息 android.os.Debug.MemoryInfo[] processMemoryInfo = am .getProcessMemoryInfo(new int[] { pid }); android.os.Debug.MemoryInfo memoryInfo = processMemoryInfo[0]; long totalPrivateDirty = memoryInfo.getTotalPrivateDirty() * 1024;// 获取占用内存大小, // kb String packageName = runningAppProcessInfo.processName;// 获取包名 taskInfo.memory = totalPrivateDirty; taskInfo.packageName = packageName; try { PackageInfo packageInfo = pm.getPackageInfo(packageName, 0); taskInfo.icon = packageInfo.applicationInfo.loadIcon(pm);// 获取图标 taskInfo.name = packageInfo.applicationInfo.loadLabel(pm) .toString();// 获取名称 } catch (Exception e) { e.printStackTrace(); // 有些系统级别的进程没有名称和图标, 获取时会抛异常, 在此捕获后设定默认图片,并将包名作为进程名称 taskInfo.icon = ctx.getResources().getDrawable( R.drawable.task_default); taskInfo.name = taskInfo.packageName; } taskInfos.add(taskInfo); } return taskInfos; }
- 展示数据
区分系统进程和用户进程, 分类展示的逻辑, 和软件管理页面相同
- 获取数据
- 全选&反选
1.增加Checkbox控件 注意: 由于listview的重用机制,导致滑动列表时checkbox勾选异常 2. TaskInfo对象增加字段isSelect,表示是否选中, 在getView方法中通过判断isSelect的值来更新checkbox的状态 3. 禁掉checkbox的点击事件,设置listView的点击监听,来更新选中状态 注意: 要先禁掉checkbox的事件,否则listview点击监听无法响应 lvList.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { TaskInfo item = mAdapter.getItem(position); CheckBox checkBox = (CheckBox) view.findViewById(R.id.cb_check); if (item.isChecked) { item.isChecked = false; checkBox.setChecked(false); } else { item.isChecked = true; checkBox.setChecked(true); } } }); 4. 全选&反选的实现 /** * 全选 */ public void selectAll(View view) { for (TaskInfo taskInfo : mSystemTaskList) { taskInfo.isChecked = true; } for (TaskInfo taskInfo : mUserTaskList) { taskInfo.isChecked = true; } mAdapter.notifyDataSetChanged(); } /** * 反选 * * @param view */ public void reverseSelect(View view) { for (TaskInfo taskInfo : mSystemTaskList) { taskInfo.isChecked = !taskInfo.isChecked; } for (TaskInfo taskInfo : mUserTaskList) { taskInfo.isChecked = !taskInfo.isChecked; } mAdapter.notifyDataSetChanged(); } 5. 注意: 本应用不允许被勾选 - getView中隐藏本应用的checkbox - listview点击监听时跳过本应用 - 全选/反选时跳过本应用
- 一键清理
/** * 一键清理 * 需要权限:android.permission.KILL_BACKGROUND_PROCESSES * @param view */ public void killAll(View view) { ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); ArrayList<TaskInfo> killedTasks = new ArrayList<TaskInfo>(); // 清除用户进程 for (TaskInfo taskInfo : mUserTaskList) { if (taskInfo.isChecked) { // android.os.Process.killProcess(android.os.Process.myPid());//app自杀 am.killBackgroundProcesses(taskInfo.packageName); killedTasks.add(taskInfo); } } // 清除系统进程 for (TaskInfo taskInfo : mSystemTaskList) { if (taskInfo.isChecked) { am.killBackgroundProcesses(taskInfo.packageName); killedTasks.add(taskInfo); } } long savedMemory = 0; // 从列表中删除已杀死的进程 for (TaskInfo taskInfo : killedTasks) { if (taskInfo.isUserTask) { mUserTaskList.remove(taskInfo); } else { mSystemTaskList.remove(taskInfo); } savedMemory += taskInfo.memory; } Toast.makeText( this, String.format("共杀死了%d个进程,帮您释放了%s的空间!", killedTasks.size(), Formatter.formatFileSize(this, savedMemory)), Toast.LENGTH_SHORT).show(); //更新当前进程数和剩余内存 tvRunningTasks.setText("运行中的进程:" + (mRunningTaskNum - killedTasks.size()) + "个"); tvAvailMemo.setText("剩余/总内存:" + Formatter.formatFileSize(this, mAvailMemory + savedMemory) + "/" + Formatter.formatFileSize(this, mTotalMemory)); mAdapter.notifyDataSetChanged(); }
- 注意: 可用内存+手机卫士占用内存 != 总内存;为什么呢
是因为手机里还运行一些纯C的进程查看进程命令
进入模拟题指令:adb -s emulator-5554 shell
查看进程指令:ps
演示杀掉system_server:kill -9 1022
1022为进程的ID号, -9是linux下的信号, 表示立即终止进程
- 注意: 可用内存+手机卫士占用内存 != 总内存;为什么呢
-
系统进程显示和隐藏
- 创建进程管理设置页面:TaskManagerSettingActivity
- 编写设置页面布局文件
- 监听Checkbox的勾选事件,更新本地SharePreference
// 根据本地记录,更新checkbox状态 boolean showSystem = mPrefs.getBoolean("show_system_task", true); if (showSystem) { cbShowSystem.setChecked(true); cbShowSystem.setText("显示系统进程"); } else { cbShowSystem.setChecked(false); cbShowSystem.setText("不显示系统进程"); } // 设置状态勾选监听 cbShowSystem.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { cbShowSystem.setText("显示系统进程"); mPrefs.edit().putBoolean("show_system_task", true).commit(); } else { cbShowSystem.setText("不显示系统进程"); mPrefs.edit().putBoolean("show_system_task", false) .commit(); } } });
- 根据sp记录的是否显示系统进程,更新listview的显示个数
@Override public int getCount() { // 通过判断是否显示系统进程,更新list的数量 boolean showSystem = mPrefs.getBoolean("show_system_task", true); if (showSystem) { return 1 + mUserTaskList.size() + 1 + mSystemTaskList.size(); } else { return 1 + mUserTaskList.size(); } }
- 保证勾选框改变后,listview可以立即刷新
public void setting(View view) { startActivityForResult(new Intent(this, TaskManagerSettingActivity.class), 0); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // 当从设置页面回跳回来之后,刷新listview mAdapter.notifyDataSetChanged(); }
- 锁屏清理
- 演示金山进程管理效果
- 后台启动服务,监听广播
//判断锁屏清理的广播是否正在运行 boolean serviceRunning = ServiceStatusUtils.isServiceRunning( "com.itheima.mobilesafeteach.service.AutoKillService", this); if (serviceRunning) { cbLockClear.setChecked(true); cbLockClear.setText("当前状态:锁屏清理已经开启"); } else { cbLockClear.setChecked(false); cbLockClear.setText("当前状态:锁屏清理已经关闭"); } cbLockClear.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { Intent intent = new Intent(TaskManagerSettingActivity.this, AutoKillService.class); if (isChecked) { // 启动锁屏清理的服务 startService(intent); cbLockClear.setText("当前状态:锁屏清理已经开启"); } else { // 关闭锁屏清理的服务 stopService(intent); cbLockClear.setText("当前状态:锁屏清理已经关闭"); } } }); ------------------------------------- /** * 锁屏清理进程的服务 * * @author Kevin * */ public class AutoKillService extends Service { private InnerScreenOffReceiver mReceiver; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); //监听屏幕关闭的广播, 注意,该广播只能在代码中注册,不能在清单文件中注册 mReceiver = new InnerScreenOffReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); registerReceiver(mReceiver, filter); } @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); mReceiver = null; } /** * 锁屏关闭的广播接收者 * * @author Kevin * */ class InnerScreenOffReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { System.out.println("屏幕关闭..."); // 杀死后台所有运行的进程 ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); List<RunningAppProcessInfo> runningAppProcesses = am .getRunningAppProcesses(); for (RunningAppProcessInfo runningAppProcessInfo : runningAppProcesses) { am.killBackgroundProcesses(runningAppProcessInfo.processName); } } } }
- 定时器清理(介绍)
// 在AutoKillService的onCreate中启动定时器,定时清理任务 mTimer = new Timer(); mTimer.schedule(new TimerTask() { @Override public void run() { System.out.println("5秒运行一次!"); } }, 0, 5000); @Override protected void onDestroy() { super.onDestroy(); mTimer.cancel(); mTimer = null; }
- 桌面Widget(窗口小部件)
- widget介绍(Android, 瑞星,早期word)
- widget谷歌文档查看(API Guide->App Components->App Widget)
- widget开发流程
1. 在com.itheima.mobilesafe.receiver目录下创建MyWidget并且继承AppWidgetProvider 2. 在功能清单文件注册,参照文档 <receiver android:name=".receiver.MyWidget" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_info" /> </receiver> 3. 在res/xml/创建文件example_appwidget_info.xml拷贝文档内容 <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="294dp" android:minHeight="72dp"//能被调整的最小宽高,若大于minWidth minHeight 则忽略 android:updatePeriodMillis="86400000"//更新周期,毫秒,最短默认半小时 android:previewImage="@drawable/preview"//选择部件时 展示的图像,3.0以上使用,默认是ic_launcher android:initialLayout="@layout/example_appwidget"//布局文件 android:configure="com.example.android.ExampleAppWidgetConfigure"//添加widget之前,先跳转到配置的activity进行相关参数配置,这个我们暂时用不到 android:resizeMode="horizontal|vertical"//widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸 android:widgetCategory="home_screen|keyguard"//分别在屏幕主页和锁屏状态也能显示 android:initialKeyguardLayout="@layout/example_keyguard"//锁屏状态显示的样式 > </appwidget-provider> 4. 精简example_appwidget_info.xml文件,最终结果: <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="294dp" android:minHeight="72dp" android:updatePeriodMillis="1800000" android:initialLayout="@layout/appwidget" > </appwidget-provider> 5. widget布局文件:appwidget.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="wrap_content" android:layout_height="wrap_content" android:background="#f00" android:text="我是widget,哈哈哈" android:textSize="30sp" /> </LinearLayout>
- 简单演示,高低版本对比
-
仿照金山widget效果, apktool反编译,抄金山布局文件(业内抄袭成风)
1. 反编译金山apk 使用apktool,可以查看xml文件内容 apktool d xxx.apk 2. 在金山清单文件中查找 APPWIDGET_UPDATE, 找到widget注册的代码 3. 拷贝金山widget的布局文件process_widget_provider.xml到自己的项目中 4. 从金山项目中拷贝相关资源文件,解决报错 5. 运行,查看效果
- widget生命周期
/** * 窗口小部件widget * * @author Kevin * */ public class MyWidget extends AppWidgetProvider { /** * widget的每次变化都会调用onReceive */ @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); System.out.println("MyWidget: onReceive"); } /** * 当widget第一次被添加时,调用onEnable */ @Override public void onEnabled(Context context) { super.onEnabled(context); System.out.println("MyWidget: onEnabled"); } /** * 当widget完全从桌面移除时,调用onDisabled */ @Override public void onDisabled(Context context) { super.onDisabled(context); System.out.println("MyWidget: onDisabled"); } /** * 新增widget时,或者widget更新时,调用onUpdate * 更新时间取决于xml中配置的时间,最短为半小时 */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); System.out.println("MyWidget: onUpdate"); } /** * 删除widget时,调onDeleted */ @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); System.out.println("MyWidget: onDeleted"); } /** * 当widget大小发生变化时,调用此方法 */ @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { System.out.println("MyWidget: onAppWidgetOptionsChanged"); } }
- 定时更新widget
问题: 我们需要通过widget实时显示当前进程数和可用内存,但widget最短也得半个小时才会更新一次, 如何才能间隔比较短的时间来及时更新? 查看金山日志: 当桌面有金山widget时, 金山会在后台启动service:ProcessService,并定时输出如下日志: 03-29 08:43:03.070: D/MoSecurity.ProcessService(275): updateWidget 该日志在锁屏状态下也一直输出. 解决办法: 后台启动service,UpdateWidgetService, 并在service中启动定时器来控制widget的更新
- 更新widget方法
/** * 定时更新widget的service * * @author Kevin * */ public class UpdateWidgetService extends Service { private Timer mTimer; private AppWidgetManager mAWM; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); mAWM = AppWidgetManager.getInstance(this); // 启动定时器,每个5秒一更新 mTimer = new Timer(); mTimer.schedule(new TimerTask() { @Override public void run() { System.out.println("更新widget啦!"); updateWidget(); } }, 0, 5000); } /** * 更新widget */ private void updateWidget() { // 初始化远程的view对象 RemoteViews views = new RemoteViews(getPackageName(), R.layout.process_widget); views.setTextViewText(R.id.tv_running_tasks, "正在运行的软件:" + TaskInfoProvider.getRunningTaskNum(this)); views.setTextViewText( R.id.tv_memory_left, "可用内存:" + Formatter.formatFileSize(this, TaskInfoProvider.getAvailMemory(this))); // 初始化组件 ComponentName provider = new ComponentName(this, MyWidget.class); // 更新widget mAWM.updateAppWidget(provider, views); } @Override public void onDestroy() { super.onDestroy(); mTimer.cancel(); mTimer = null; } } ----------------------------- 启动和销毁service的时机 分析widget的声明周期,在onEnabled和onUpdate中启动服务, 在onDisabled中结束服务
- 注意: APK安装在sd卡上,widget在窗口小部件列表里无法显示。 android:installLocation=”preferExternal”, 修改过来后,需要卸载,再去安装widget才生效;
-
点击事件处理
// 初始化延迟意图,pending是等待的意思 Intent intent = new Intent(this, HomeActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); // 当点击widget布局时,跳转到主页面 views.setOnClickPendingIntent(R.id.ll_root, pendingIntent); //当一键清理被点击是,发送广播,清理内存 Intent btnIntent = new Intent(); btnIntent.setAction("com.itheima.mobilesafeteach.KILL_ALL"); PendingIntent btnPendingIntent = PendingIntent.getBroadcast(this, 0, btnIntent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.btn_clear, btnPendingIntent); --------------------------- /** * 杀死后台进程的广播接受者 * 清单文件中配置action="com.itheima.mobilesafeteach.KILL_ALL" * * @author Kevin * */ public class KillAllReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { System.out.println("kill all..."); // 杀死后台所有运行的进程 TaskInfoProvider.killAll(context); } } --------------------------- <receiver android:name=".receiver.KillAllReceiver" > <intent-filter> <action android:name="com.itheima.mobilesafeteach.KILL_ALL" /> </intent-filter> </receiver>
- 做一个有情怀的程序员, 拒绝耗电!
当锁屏关闭时,停止widget定时器的更新 UpdateWidgetService: // 注册屏幕开启和关闭的广播接受者 mReceiver = new InnerScreenReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); registerReceiver(mReceiver, filter); /** * 屏幕关闭和开启的广播接收者 * * @author Kevin * */ class InnerScreenReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (Intent.ACTION_SCREEN_OFF.equals(action)) {// 屏幕关闭 if (mTimer != null) { // 停止定时器 mTimer.cancel(); mTimer = null; } } else {// 屏幕开启 startTimer(); } } }
Day10
- 程序锁
- 腾讯管家软件加锁功能演示
- 高级工具中添加程序锁入口
- 新建程序锁页面 AppLockActivity
- 程序锁页面布局文件实现
activity_app_lock.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" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="horizontal" > <TextView android:id="@+id/tv_unlock" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/tab_left_pressed" android:gravity="center" android:text="未加锁" android:textColor="#fff" /> <TextView android:id="@+id/tv_locked" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/tab_right_default" android:gravity="center" android:text="已加锁" android:textColor="#fff" /> </LinearLayout> <LinearLayout android:id="@+id/ll_unlock" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="未加锁软件:x个" android:textColor="#000" /> <ListView android:id="@+id/lv_unlock" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> <LinearLayout android:id="@+id/ll_locked" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="已加锁软件:x个" android:textColor="#000" /> <ListView android:id="@+id/lv_locked" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> </LinearLayout>
- 点击标签切换页面
@Override public void onClick(View v) { switch (v.getId()) { case R.id.tv_unlock:// 展示未加锁页面,隐藏已加锁页面 llLocked.setVisibility(View.GONE); llUnlock.setVisibility(View.VISIBLE); tvUnlock.setBackgroundResource(R.drawable.tab_left_pressed); tvLocked.setBackgroundResource(R.drawable.tab_right_default); break; case R.id.tv_locked:// 展示已加锁页面,隐藏未加锁页面 llUnlock.setVisibility(View.GONE); llLocked.setVisibility(View.VISIBLE); tvUnlock.setBackgroundResource(R.drawable.tab_left_default); tvLocked.setBackgroundResource(R.drawable.tab_right_pressed); break; default: break; } }
- 应用列表信息展现(展现全部应用列表数据)
-
使用数据库保存已加锁的软件
AppLockOpenHelper.java // 创建表, 两个字段,_id, packagename(应用包名) db.execSQL("create table applock (_id integer primary key autoincrement, packagename varchar(50))"); ---------------------------------- AppLockDao.java(逻辑和黑名单列表类似) /** * 增加程序锁应用 */ public void add(String packageName) { SQLiteDatabase db = mHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("packagename", packageName); db.insert("applock", null, values); db.close(); } /** * 删除程序锁应用 * * @param number */ public void delete(String packageName) { SQLiteDatabase db = mHelper.getWritableDatabase(); db.delete("applock", "packagename=?", new String[] { packageName }); db.close(); } /** * 查找程序锁应用 * * @param number * @return */ public boolean find(String packageName) { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db.query("applock", null, "packagename=?", new String[] { packageName }, null, null, null); boolean result = false; if (cursor.moveToFirst()) { result = true; } cursor.close(); db.close(); return result; } /** * 查找已加锁列表 * * @return */ public ArrayList<String> findAll() { SQLiteDatabase db = mHelper.getWritableDatabase(); Cursor cursor = db.query("applock", new String[] { "packagename" }, null, null, null, null, null); ArrayList<String> list = new ArrayList<String>(); while (cursor.moveToNext()) { String packageName = cursor.getString(0); list.add(packageName); } cursor.close(); db.close(); return list; }
- 监听list item点击事件,向数据库添加一些数据
lvUnLock.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { AppInfo info = mUnlockList.get(position); mDao.add(info.packageName); } });
- 已加锁和未加锁数据设置
private ArrayList<AppInfo> mLockedList;// 已加锁列表集合 private ArrayList<AppInfo> mUnlockList;// 未加锁列表集合 private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { // 设置未加锁数据 mUnlockAdapter = new AppLockAdapter(false); lvUnLock.setAdapter(mUnlockAdapter); // 设置已加锁数据 mLockedAdapter = new AppLockAdapter(true); lvLocked.setAdapter(mLockedAdapter); }; }; /** * 初始化应用列表数据 */ private void initData() { new Thread() { @Override public void run() { mList = AppInfoProvider.getAppInfos(AppLockActivity.this); mLockedList = new ArrayList<AppInfo>(); mUnlockList = new ArrayList<AppInfo>(); for (AppInfo info : mList) { boolean isLocked = mDao.find(info.packageName); if (isLocked) { mLockedList.add(info); } else { mUnlockList.add(info); } } mHandler.sendEmptyMessage(0); } }.start(); }
- 界面效果完善
点击锁子图标后, 实现加锁和去加锁的逻辑, 界面跟着更新 class AppLockAdapter extends BaseAdapter { private boolean isLocked;//true表示已加锁数据 public AppLockAdapter(boolean isLocked) { this.isLocked = isLocked; } @Override public int getCount() { if (isLocked) { return mLockedList.size(); } else { return mUnlockList.size(); } } @Override public AppInfo getItem(int position) { if (isLocked) { return mLockedList.get(position); } else { return mUnlockList.get(position); } } @Override public long getItemId(int position) { return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = View.inflate(AppLockActivity.this, R.layout.list_applock_item, null); holder = new ViewHolder(); holder.ivIcon = (ImageView) convertView .findViewById(R.id.iv_icon); holder.tvName = (TextView) convertView .findViewById(R.id.tv_name); holder.ivLock = (ImageView) convertView .findViewById(R.id.iv_lock); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } final AppInfo info = getItem(position); holder.ivIcon.setImageDrawable(info.icon); holder.tvName.setText(info.name); if(isLocked) { holder.ivLock.setImageResource(R.drawable.unlock); }else { holder.ivLock.setImageResource(R.drawable.lock); } holder.ivLock.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (isLocked) { mDao.delete(info.packageName);// 从数据库删除记录 mLockedList.remove(info);// 从已加锁集合删除元素 mUnlockList.add(info);// 给未加锁集合添加元素 } else { mDao.add(info.packageName);// 向数据库添加记录 mLockedList.add(info);// 给已加锁集合添加元素 mUnlockList.remove(info);// 从未加锁集合删除元素 } // 刷新listview mLockedAdapter.notifyDataSetChanged(); mUnlockAdapter.notifyDataSetChanged(); } }); return convertView; } }
- 更新已加锁/未加锁数量
/** * 更新已加锁和未加锁数量 */ private void updateAppNum() { tvUnLockNum.setText("未加锁软件:" + mUnlockList.size() + "个"); tvLockedNum.setText("已加锁软件:" + mLockedList.size() + "个"); } // 每次刷新listview前都会调用getCount方法,可以在这里更新数量 @Override public int getCount() { updateAppNum(); if (isLocked) { return mLockedList.size(); } else { return mUnlockList.size(); } }
- 动画实现
- 解决动画移动问题 导致的原因,动画没有开始播放,界面就刷新了。 动画播放需要时间的,动画没有播就变成了新的View对象。就播了新的View对象, 让动画播放完后,再去更新页面; public AppLockAdapter(boolean isLocked) { this.isLocked = isLocked; // 右移 mLockAnim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0); mLockAnim.setDuration(500); // 左移 mUnLockAnim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0); mUnLockAnim.setDuration(500); } holder.ivLock.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (isLocked) { view.startAnimation(mUnLockAnim); mUnLockAnim .setAnimationListener(new AnimationListener() { @Override public void onAnimationStart( Animation animation) { } @Override public void onAnimationRepeat( Animation animation) { } //监听动画结束事件 @Override public void onAnimationEnd( Animation animation) { mDao.delete(info.packageName);// 从数据库删除记录 mLockedList.remove(info);// 从已加锁集合删除元素 mUnlockList.add(info);// 给未加锁集合添加元素 // 刷新listview mLockedAdapter.notifyDataSetChanged(); mUnlockAdapter.notifyDataSetChanged(); } }); } else { view.startAnimation(mLockAnim); mLockAnim.setAnimationListener(new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } //监听动画结束事件 @Override public void onAnimationEnd(Animation animation) { mDao.add(info.packageName);// 向数据库添加记录 mLockedList.add(info);// 给已加锁集合添加元素 mUnlockList.remove(info);// 从未加锁集合删除元素 // 刷新listview mLockedAdapter.notifyDataSetChanged(); mUnlockAdapter.notifyDataSetChanged(); } }); } } });
- 看门狗
- 看门狗原理介绍
- 创建服务WatchDogService
- 设置页面增加启动服务的开关
- 看门狗轮询检测任务栈
打印当前最顶上的activity /** * 看门狗服务 需要权限: android.permission.GET_TASKS * * @author Kevin * */ public class WathDogService extends Service { private boolean isRunning;// 表示线程是否正在运行 private ActivityManager mAM; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); mAM = (ActivityManager) getSystemService(ACTIVITY_SERVICE); isRunning = true; new Thread() { public void run() { while (isRunning) {// 看门狗每隔100毫秒巡逻一次 List<RunningTaskInfo> runningTasks = mAM.getRunningTasks(1);// 获取正在运行的任务栈 String packageName = runningTasks.get(0).topActivity .getPackageName();// 获取任务栈最上层activity的包名 System.out.println("top Activity=" + packageName); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }; }.start(); } @Override public void onDestroy() { super.onDestroy(); isRunning = false;// 结束线程 } }
- 轮询获取最近的task, 如果发现是加锁的,跳EnterPwdActivity
if (mDao.find(packageName)) {// 查看当前页面是否在加锁的数据库中 Intent intent = new Intent(WatchDogService.this, EnterPwdActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("packageName", packageName); startActivity(intent); } ----------------------------------- /** * 加锁输入密码页面 * * @author Kevin * */ public class EnterPwdActivity extends Activity { private TextView tvName; private ImageView ivIcon; private EditText etPwd; private Button btnOK; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_enter_pwd); tvName = (TextView) findViewById(R.id.tv_name); ivIcon = (ImageView) findViewById(R.id.iv_icon); etPwd = (EditText) findViewById(R.id.et_pwd); btnOK = (Button) findViewById(R.id.btn_ok); Intent intent = getIntent(); String packageName = intent.getStringExtra("packageName"); PackageManager pm = getPackageManager(); try { ApplicationInfo info = pm.getApplicationInfo(packageName, 0);// 根据包名获取应用信息 Drawable icon = info.loadIcon(pm);// 加载应用图标 ivIcon.setImageDrawable(icon); String name = info.loadLabel(pm).toString();// 加载应用名称 tvName.setText(name); } catch (NameNotFoundException e) { e.printStackTrace(); } btnOK.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String pwd = etPwd.getText().toString().trim(); if (!TextUtils.isEmpty(pwd)) {// 密码校验 if (pwd.equals("123")) { finish(); } else { Toast.makeText(EnterPwdActivity.this, "密码错误", Toast.LENGTH_LONG).show(); } } else { Toast.makeText(EnterPwdActivity.this, "请输入密码", Toast.LENGTH_LONG).show(); } } }); } }
- 重写返回事件,跳转到主页面
//查看系统Launcher源码,确定跳转逻辑 @Override public void onBackPressed() { // 跳转主页面 Intent intent = new Intent(); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); startActivity(intent); finish();//销毁当前页面 }
- 发送广播,看门狗跳过检测
认证成功后,发送广播 EnterPwdActivity.java // 发送广播,通知看门狗不要再拦截当前应用 Intent intent = new Intent(); intent.setAction("com.itheima.mobilesafeteach.ACTION_STOP_PROTECT"); intent.putExtra("packageName", packageName); sendBroadcast(intent); ------------------------------------------- WatchDogService.java class InnerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // 看门狗得到了消息,临时的停止对某个应用程序的保护 mSkipPackageName = intent.getStringExtra("packageName"); } } if (packageName.equals(mSkipPackageName)) {// 用过已经认证通过了,需跳过验证 System.out.println("无需验证..."); continue; }
- 相关优化
知识拓展:看门狗后台一直在运行,这样是比较耗电的。 我们要优化的的话怎么做呢? 在看门狗服务里,监听锁屏事件,如果锁屏了我就把看门狗停止(flag = false;);屏幕开启了,我就让看门狗开始工作启动服务并且flag = true;; 避免一次输入密码了不再输入;防止别人在我使用的时候,接着使用不用输入密码的情形; 也可以在锁屏的时候把mSkipPackageName赋值为空就行了。
- 利用activity启动模式修复密码输入bug
1. 演示bug(进入手机卫士,按home退到后台,然后再打开加锁app,进入后发现跳转到手机卫士页面) 2. 画图分析,正常情况下的任务栈和bug时的任务栈图; 3. 解决问题;在功能清单文件EnterPwdActivity加上字段 <activity android:name="com.itheima.mobilesafe.EnterPwdActivity" android:launchMode="singleInstance"/> 4. 然后再画图分析正确的任务栈;
- 隐藏最近打开的activity
长按小房子键:弹出历史记录页面,就会列出最近打开的Activity; 1. 演示由于最近打开的Activity导致的Bug; 2. 容易暴露用户的隐私 最近打开的Activity,是为了用户可以很快打开最近打开的应用而设计的;2.2、2.3普及后就把问题暴露出来了,很容易暴露用户的隐私。比如你玩一些日本开发的游戏:吹裙子、扒衣服这类游戏。你正在玩这些有些,这个时候,爸妈或者大学女辅导员过来了,赶紧按小房子,打开背单词的应用,这时大学女辅导员走过来说,干嘛呢,把手机交出来,长按一下小房子键,这个时候很尴尬的事情就产生了。 A:低版本是无法移除的。低版本记录最近8个;想要隐藏隐私,打开多个挤出去; B:4.0以后高版本就可以直接移除了。考虑用户呼声比较高。 3. 设置不再最近任务列表显示activity <activity android:excludeFromRecents="true" android:name="com.itheima.mobilesafe.EnterPwdActivity" android:launchMode="singleInstance" /> 4. 在装有腾讯管家的模拟器演示腾讯管理的程序锁功能;也没用现实最近的Activity,它也是这样做的。 知识拓展,以后开发带有隐私的软件,或者软件名称不好听的应用,就可以加载在最近打开列表不包括字段.
- 腾讯管家和手机卫士同时加锁,谁更快?
腾讯管家会更快一些, 所以需要再进一步优化
- 提高性能
- 缩短每次巡逻时间
//将100改为20 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
- 不频繁调用数据库
从数据库中读取所有已加锁应用列表,每次从集合中查询判断 mLockedPackages = mDao.getInstance(this).findAll();// 查询所有已加锁的应用列表 // if (mDao.find(packageName)) {// 查看当前页面是否在加锁的数据库中 if (mLockedPackages.contains(packageName)) {}
- 重新和腾讯管家比拼速度
-
监听数据库变化, 更新集合
- 增加另外一款软件进入程序锁。打开看看,是无法打开输入密码页面的;解析原因;
这个时候就需要根据数据库的数据变化而改变集合的信息了,就用到了观察者; -
联想监听来电拦截时,监听通话日志变化的逻辑,解释原理
-
具体实现
AppLockDao.java // 数据库改变后发送通知 mContext.getContentResolver().notifyChange( Uri.parse("content://com.itheima.mobilesafe/applockdb"), null); ------------------------------------- WatchDogService.java // 监听程序锁数据库内容变化 mObserver = new MyContentObserver(new Handler()); getContentResolver().registerContentObserver( Uri.parse("content://com.itheima.mobilesafe/applockdb"), true, mObserver); getContentResolver().unregisterContentObserver(mObserver);// 注销观察者 class MyContentObserver extends ContentObserver { public MyContentObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange) { System.out.println("数据变化了..."); mLockedPackages = mDao.getInstance(WatchDogService.this).findAll();// 查询所有已加锁的应用列表 } }
- 增加另外一款软件进入程序锁。打开看看,是无法打开输入密码页面的;解析原因;
- 缩短每次巡逻时间
Day11
- 流量统计
- 流量统计介绍, pc网络连接流量展示(已发送,已接受)
- 连接真机,查看文件proc/uid_stat,发现该目录下有很多以uid命名的文件夹
- 用户id是安装应用程序的时候 操作系统赋给应用程序的
- 获取uid方式
1. 进入AppInfoProvider, String name = packInfo.applicationInfo.loadLabel(pm).toString()+ packInfo.applicationInfo.uid; 2. 将应用名称和uid拼成一个字符串输出,真机查看主流应用(微信,QQ)的uid 3. 例如 QQ:10083, 进入QQ(10083)目录命令:cd 10083 4. 进入QQ10083:cd 10110 下载:168251 上传:23544 tcp_rcv :代码下载的数据 tcp_snd:代表上传的数据
- 手机安装360, 验证流量准确性
- 创建TrafficeManagerActivity
-
流量统计的api介绍
TrafficStats.getMobileRxBytes();// 3g/2g下载总流量 TrafficStats.getMobileTxBytes();// 3g/2g上传总流量 TrafficStats.getTotalRxBytes();// wifi+手机下载流量 TrafficStats.getTotalTxBytes();// wifi+手机上传总流量 TrafficStats.getUidRxBytes(10085);// 某个应用下载流量 TrafficStats.getUidTxBytes(10085);// 某个应用上传流量 这里需要注意的是,通过 TrafficStats 获取的数据在手机重启的时候会被清空,所以,如果要对流量进行持续的统计需要将数据保存到数据库中,在手机重启时将数据读出进行累加即可
- 流量报警原理简介
流量校准的工作原理就给运营商发短信 A:开启超额提醒 B:设置每月流量套餐300MB C:自动校准流量-流量短信设置 D:演示发短信给运营商;
- 联网防火墙简介
在linux上有一款强大的防火墙软件iptable 360就是把这款软件内置了 如果手机有root权限,把防火墙软件装到手机的内部,并且开启起来。 以后就可以拦截某个应用程序的联网了。 如果允许某个软件上网就什么也不做。如果不允许某个软件上网,就把这个软件的所有的联网操作都定向到本地,这时就不会产生流量了。
- Android下的开源防火墙项目droidwall
登录code.google.com,搜索droidwall svn地址: https://droidwall.googlecode.com/svn/ svn检出, 需要翻墙 常用开源代码网站: github.com, code.google.com
- 抽屉效果 SlidingDrawer
- 基本实现
<SlidingDrawer android:id="@+id/slidingDrawer1" android:layout_width="match_parent" android:layout_height="match_parent" android:content="@+id/content" android:handle="@+id/handle" > //指定抽屉把手 <ImageView android:id="@id/handle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/lock" /> //指定抽屉内容 <LinearLayout android:id="@id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#9e9e9e" android:gravity="center" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="我是小抽屉" /> </LinearLayout> </SlidingDrawer>
- 把抽屉做成从右向左拉
android:orientation="horizontal"
- 实现腾讯抽屉竖直方向显示一小半功能
只需在抽屉上方增加一个空view <View android:layout_width="match_parent" android:layout_height="200dp" />
- 水平方向显示一小半
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <View android:layout_width="100dp" android:layout_height="match_parent" /> <SlidingDrawer android:id="@+id/slidingDrawer1" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:content="@+id/content" android:handle="@+id/handle" > <ImageView android:id="@id/handle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/lock" /> <LinearLayout android:id="@id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#9e9e9e" android:gravity="center" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="我是小抽屉" /> </LinearLayout> </SlidingDrawer>
-
小锁图片显示上面
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/lock" />
- 基本实现
-
手机杀毒
- 什么是病毒?
计算机病毒是一个程序,一段可执行码。就像生物病毒一样,具有自我繁殖、互相传染以及激活再生等生物病毒特征。计算机病毒有独特的复制能力,它们能够快速蔓延,又常常难以根除。它们能把自身附着在各种类型的文件上,当文件被复制或从一个用户传送到另一个用户时,它们就随同文件一起蔓延开来。
- 计算机第一个病毒
诞生于麻省理工大学
- 蠕虫病毒
熊猫烧香,蠕虫病毒的一种,感染电脑上的很多文件;exe文件被感染,html文件被感染。 主要目的:证明技术有多牛。写这种病毒的人越来越少了
- 木马
盗窃信息,盗号、窃取隐私、偷钱,玩了一个游戏,买了很多装备,监听你的键盘输入,下次进入的话,装备全部没了。 主要目目的:挣钱,产生利益;
- 灰鸽子
主要特征,控制别人电脑,为我所有。比如挖金矿游戏挣钱的,控制几十万台机器为你干活。 总会比银河处理器快的多。 特点是:不知情情况下安装下的。
- 所有的病毒,都是执行后才有危害,如果病毒下载了,没有安装运行,是没有危害的。
-
杀毒原理介绍
定位出特殊的程序,把程序的文件给删除。 王江民, 江民杀毒软件 Kv300 Kv300 干掉300个病毒 开发kv300后很多人用盗版的。 江民炸弹
- 病毒怎么找到?-收集病毒的样本
电信 网络运营商主节点 部署服务器集群(蜜罐) 一组没有防火墙 没有安全软件 没有补丁的服务器, 主动联网,下载一些软件运行。这样情况下,特别容易中病毒。 工作原理相当于:苍蝇纸
- 360互联网云安全计划
所有的用户都是你的蜜罐; 收集的数据量就大大提高了; 国内安全厂商,有些没有职业道德。 收集一些个人隐私,或者商业机密的文件也收集过去 3Q大战
- 传统杀毒软件的缺陷
目前卡巴斯基病毒库已经有了2千多万病毒 传统杀毒软件的缺陷: 病毒数据库越来越大; 只能查杀已知的病毒,不能查杀未知病毒; 360免杀 写了一个木马,在加一个壳,加壳后360就识别不了了
- 主动防御
检查软件 1.检查开启启动项 2.检查注册表; 3.检查进程列表 病毒特征: 1、开启启动 2、隐藏自身 3、监视键盘 4、联网发邮件 启发式扫描-扫描单个文件 拷贝文件到虚拟机-相当于精简版的系统, 运行后检测是否具备病毒特点
- 杀毒引擎
优化后的数据库查询算法,优先扫描当下最常见的病毒, 速度快
- Android上的杀毒软件
大多数停留在基于数据库方式杀毒 LBE主动防御方式杀毒。敏感权限扫描,敏感操作提示,和小米深度合作 金山手机卫士病毒库
- 什么是病毒?
- 手机杀毒模块开发
- 创建AntiVirusActivity
- 布局文件开发
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="10dp" > <FrameLayout android:layout_width="80dp" android:layout_height="80dp" > <ImageView android:id="@+id/imageView1" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/ic_scanner_malware" /> <ImageView android:id="@+id/iv_scanning" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/act_scanning_03" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" > <TextView android:id="@+id/tv_scan_status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:text="正在初始化8核杀毒引擎" android:textColor="#000" android:textSize="18sp" /> <ProgressBar android:id="@+id/progressBar1" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" /> </LinearLayout>
-
扫描动画
RotateAnimation anim = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); anim.setDuration(1000);//间隔时间 anim.setRepeatCount(Animation.INFINITE);//无限循环 anim.setInterpolator(new LinearInterpolator());//匀速循环,不停顿 ivScanning.startAnimation(anim);
- 自定义进度条样式
1. 查看android系统对Progressbar样式的定义 开发环境\platforms\android-16\data\res\values\styles.xml,搜索Widget.Holo.ProgressBar.Horizontal->progress_horizontal_holo_light 2. 拷贝xml文件,修改成自己的图片 <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> <item android:id="@android:id/progress" android:drawable="@drawable/security_progress"> </item> </layer-list> 3. 将xml文件设置给Progressbar <ProgressBar android:id="@+id/progressBar1" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:progress="50" android:layout_marginTop="5dp" android:progressDrawable="@drawable/custom_progress" /> 4. 进度更新 // 更新进度条 new Thread() { public void run() { pbProgress.setMax(100); for (int i = 0; i <= 100; i++) { pbProgress.setProgress(i); try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } }; }.start();
- 病毒签名
- 签名知识回顾
- 安装rocket.apk, 运行正常, 然后卸载
- 打开rocket.apk安装包,篡改启动图片,重新安装, Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]
- 分析安装包中的签名文件信息
进入META-INF的文件夹,这个里边存储的是关于签名的一些信息. (1)MANIFEST.MF:这是摘要文件。程序遍历Apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个用SHA1生成摘要信息,再用Base64进行编码。如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。 说明:如果攻击者修改了程序的内容,有重新生成了新的摘要,那么就可以通过验证,所以这是一个非常简单的验证。 (2)CERT.SF:这是对摘要的签名文件。对前一步生成的MANIFEST.MF,使用SHA1-RSA算法,用开发者的私钥进行签名。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息(即,MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被异常修改。 说明:在这一步,即使开发者修改了程序内容,并生成了新的摘要文件,但是攻击者没有开发者的私钥,所以不能生成正确的签名文件(CERT.SF)。系统在对程序进行验证的时候,用开发者公钥对不正确的签名文件进行解密,得到的结果和摘要文件(MANIFEST.MF)对应不起来,所以不能通过检验,不能成功安装文件。 (3)CERT.RSA文件中保存了公钥、所采用的加密算法等信息。 说明:系统对签名文件进行解密,所需要的公钥就是从这个文件里取出来的。 结论:从上面的总结可以看出,META-INFO里面的说那个文件环环相扣,从而保证Android程序的安全性。(只是防止开发者的程序不被攻击者修改,如果开发者的公私钥对对攻击者得到或者开发者开发出攻击程序,Android系统都无法检测出来。)
- 获取系统安装包的签名信息
PackageManager pm = getPackageManager(); // 获取所有已安装/未安装的包的签名信息 // GET_UNINSTALLED_PACKAGES代表已删除,但还有安装目录的 List<PackageInfo> packages = pm .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES + PackageManager.GET_SIGNATURES); for (PackageInfo packageInfo : packages) { Signature[] signatures = packageInfo.signatures; System.out.println(packageInfo.applicationInfo.loadLabel(pm)); System.out.println(signatures[0].toCharsString()); System.out.println("----------"); } 将签名信息拷贝到文本编辑器,发现多个系统app共用一个签名文件 由于签名信息过长, 可以对签名进行MD5加密后输出 String signature = MD5Utils.encode(signatures[0].toCharsString());
- 签名知识回顾
- 扫描病毒数据库
AntiVirusDao.java /** * 病毒数据库的封装 * * @author Kevin * */ public class AntiVirusDao { public static final String PATH = "data/data/com.itheima.mobilesafeteach/files/antivirus.db"; /** * 根据签名的md5判断是否是病毒 * * @param md5 * @return 返回病毒描述,如果不是病毒,返回null */ public static String isVirus(String md5) { SQLiteDatabase db = SQLiteDatabase.openDatabase(PATH, null, SQLiteDatabase.OPEN_READONLY); Cursor cursor = db.rawQuery("select desc from datable where md5=? ", new String[] { md5 }); String desc = null; if (cursor.moveToFirst()) { desc = cursor.getString(0); } cursor.close(); db.close(); return desc; } }
- 扫描安装包并更新进度条
int progress = 0; Random random = new Random(); for (PackageInfo packageInfo : packages) { String name = packageInfo.applicationInfo.loadLabel(pm) .toString(); Signature[] signatures = packageInfo.signatures; String signature = MD5Utils.encode(signatures[0] .toCharsString()); String desc = AntiVirusDao.isVirus(signature); if (desc != null) { // 是病毒 System.out.println("是病毒...."); } else { // 不是病毒 System.out.println("不是病毒...."); } progress++; pbProgress.setProgress(progress); try { Thread.sleep(50 + random.nextInt(50));//随机休眠一段时间 } catch (InterruptedException e) { e.printStackTrace(); } }
- 扫描过程中,更新扫描状态文字
- 扫描前,强制休眠2秒,展示"正在初始化8核杀毒引擎" - 使用handler发送消息,更新TextView为:正在扫描:应用名称 - 扫描结束后, 发送消息,更新TextView为:扫描完毕 - 扫描结束后,关闭扫描的动画
- 扫描过程中,更新扫描文件列表
- 布局文件中添加空的LinearLayout(竖直方向),动态给线性布局添加TextView - 使用ScrollView包裹线性布局,保证可以上下滑动 <ScrollView android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:id="@+id/ll_scanning" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > </LinearLayout> </ScrollView> - 如果发现是病毒, TextView需要展示为红色, 为了区分是不是病毒,可以把扫描的文件封装成一个对象ScanInfo class ScanInfo { public String packageName; public String desc; public String name; public boolean isVirus; } private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { switch (msg.what) { case SCANNING: ScanInfo info = (ScanInfo) msg.obj; tvScanStatus.setText("正在扫描:" + info.name); TextView tvScan = new TextView(AntiVirusActivity.this); if (info.isVirus) { tvScan.setText("发现病毒:" + info.name); tvScan.setTextColor(Color.RED); } else { tvScan.setText("扫描安全:" + info.name); } llScanning.addView(tvScan); break; case SCANNING_FINISHED: tvScanStatus.setText("扫描完毕"); ivScanning.clearAnimation();// 清除扫描的动画 break; default: break; } }; };
- 制作病毒
制作两个apk文件,使用特定签名进行打包,并将该签名的md5加入到病毒数据库中,这样的话就可以测试扫出病毒的情况了 注意: 将原来的antivirus.db替换为新的文件后,一定要把app的数据清除后再运行,重新进行拷贝数据库的操作, 否则app仍找的是data/data目录下的旧版数据库!
- 创建病毒集合
-
发现病毒后,提示用户删除病毒
if (mVirusList.isEmpty()) { Toast.makeText(getApplicationContext(), "你的手机很安全了,继续加油哦!", Toast.LENGTH_SHORT).show(); } else { showAlertDialog(); } ---------------------------- /** * 发现病毒后,弹出警告弹窗 */ protected void showAlertDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("警告!"); builder.setMessage("发现" + mVirusList.size() + "个病毒, 非常危险,赶紧清理!"); builder.setPositiveButton("立即清理", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { for (ScanInfo info : mVirusList) { // 卸载apk Intent intent = new Intent(Intent.ACTION_DELETE); intent.setData(Uri.parse("package:" + info.packageName)); startActivity(intent); } } }); builder.setNegativeButton("下次再说", null); AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false);// 点击弹窗外面,弹窗不消失 dialog.show(); }
- 处理横竖屏切换
fn+ctrl+f11 切换模拟器横竖屏后, Activity的onCreate方法会从新走一次, 可以通过清单文件配置,Activity强制显示竖屏 <activity android:name=".activity.AntiVirusActivity" android:screenOrientation="portrait" /> 或者, 可以显示横屏, 通过此配置可以不重新创建Activity <activity android:name=".activity.AntiVirusActivity" android:configChanges="orientation|screenSize|keyboardHidden" />
- 缓存清理功能介绍
- 演示金山卫士缓存清理功能
- 新建缓存Demo,并通过金山卫士扫描
File file = new File(getCacheDir(), "cache.txt"); FileOutputStream out = null; try { out = new FileOutputStream(file); out.write("jfklsdjlkfds".getBytes()); out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (Exception e) { e.printStackTrace(); } }
- 新建工程,获取缓存大小
- 布局文件开发
<EditText android:id="@+id/et_package" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入包名" > </EditText> <Button android:id="@+id/btn_ok" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="确定" /> <TextView android:id="@+id/tv_result" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="查询结果" />
-
查看系统设置源码, 查看清理缓存逻辑
- 导入Setting源码
- 查找清除缓存的逻辑
Clear cache->clear_cache_btn_text->installed_app_details->InstalledAppDetails->cache_size_text-> mAppEntry.cacheSize->stats.cacheSize->stats->mStatsObserver->getPackageSizeInfo->查看PackageManager源码,跟踪方法getPackageSizeInfo,发现改方法隐藏
- 通过反射方式,调用PackageManager的方法
public Method[] getMethods()返回某个类的所有公用(public)方法包括其继承类的公用方法,当然也包括它所实现接口的方法。 public Method[] getDeclaredMethods()对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。当然也包括它所实现接口的方法。 //需要权限:android.permission.GET_PACKAGE_SIZE btnOk.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String packageName = etPackage.getText().toString().trim(); if (!TextUtils.isEmpty(packageName)) { PackageManager pm = getPackageManager(); try { Method method = pm.getClass().getDeclaredMethod( "getPackageSizeInfo", String.class, IPackageStatsObserver.class); method.invoke(pm, packageName, new MyObserver()); } catch (Exception e) { e.printStackTrace(); } } else { Toast.makeText(getApplicationContext(), "输入内容不能为空!", Toast.LENGTH_SHORT).show(); } } }); ------------------------------------- class MyObserver extends IPackageStatsObserver.Stub { // 在子线程运行 @Override public void onGetStatsCompleted(PackageStats pStats, boolean succeeded) throws RemoteException { long cacheSize = pStats.cacheSize; long dataSize = pStats.dataSize; long codeSize = pStats.codeSize; String result = "缓存:" + Formatter.formatFileSize(getApplicationContext(), cacheSize) + "\n" + "数据:" + Formatter.formatFileSize(getApplicationContext(), dataSize) + "\n" + "代码:" + Formatter.formatFileSize(getApplicationContext(), codeSize); System.out.println(result); Message msg = Message.obtain(); msg.obj = result; mHandler.sendMessage(msg); } }
- 布局文件开发
- 缓存清理模块开发
- 新建页面CleanCacheActivity
- 布局文件开发
<?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" > <RelativeLayout android:layout_width="match_parent" android:layout_height="50dp" android:background="#8866ff00" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:text="缓存清理" android:textColor="#000" android:textSize="22sp" /> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="5dp" android:onClick="cleanCache" android:text="立即清理" /> </RelativeLayout> <ProgressBar android:id="@+id/pb_progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:progressDrawable="@drawable/custom_progress" /> <TextView android:id="@+id/tv_status" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="正在扫描:" /> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:id="@+id/ll_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > </LinearLayout> </ScrollView> </LinearLayout>
- 缓存页面逻辑
private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { switch (msg.what) { case SCANNING: String name = (String) msg.obj; tvStatus.setText("正在扫描:" + name); break; case SHOW_CACHE_INFO: CacheInfo info = (CacheInfo) msg.obj; View itemView = View.inflate(getApplicationContext(), R.layout.list_cacheinfo_item, null); TextView tvName = (TextView) itemView .findViewById(R.id.tv_name); ImageView ivIcon = (ImageView) itemView .findViewById(R.id.iv_icon); TextView tvCache = (TextView) itemView .findViewById(R.id.tv_cache_size); ImageView ivDelete = (ImageView) itemView .findViewById(R.id.iv_delete); tvName.setText(info.name); ivIcon.setImageDrawable(info.icon); tvCache.setText("缓存大小:" + Formatter.formatFileSize(getApplicationContext(), info.cacheSize)); llContainer.addView(itemView); break; case SCANNING_FINISHED: tvStatus.setText("扫描完成"); break; default: break; } }; }; /** * 开始扫描 */ private void startScan() { new Thread() { @Override public void run() { List<PackageInfo> packages = mPM .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); pbProgress.setMax(packages.size());// 设置进度条最大值为安装包的数量 int progress = 0; for (PackageInfo packageInfo : packages) { try { Method method = mPM.getClass().getMethod( "getPackageSizeInfo", String.class, IPackageStatsObserver.class); method.invoke(mPM, packageInfo.packageName, new MyObserver()); } catch (Exception e) { e.printStackTrace(); } progress++; pbProgress.setProgress(progress); // 发送更新进度的消息 Message msg = Message.obtain(); msg.what = SCANNING; msg.obj = packageInfo.applicationInfo.loadLabel(mPM) .toString(); mHandler.sendMessage(msg); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } // 发送扫描结束的消息 mHandler.sendEmptyMessage(SCANNING_FINISHED); } }.start(); } class MyObserver extends IPackageStatsObserver.Stub { // 在子线程运行 @Override public void onGetStatsCompleted(PackageStats pStats, boolean succeeded) throws RemoteException { long cacheSize = pStats.cacheSize;// 获取缓存大小 if (cacheSize > 0) { try { CacheInfo info = new CacheInfo(); String packageName = pStats.packageName; info.packageName = packageName; ApplicationInfo applicationInfo = mPM.getApplicationInfo( packageName, 0); info.name = applicationInfo.loadLabel(mPM).toString(); info.icon = applicationInfo.loadIcon(mPM); info.cacheSize = cacheSize; // 扫描到缓存应用时发送消息 Message msg = Message.obtain(); msg.what = SHOW_CACHE_INFO; msg.obj = info; mHandler.sendMessage(msg); } catch (NameNotFoundException e) { e.printStackTrace(); } } } } // 缓存对象的封装 class CacheInfo { public String name; public String packageName; public Drawable icon; public long cacheSize; } ------------------------- list_cacheinfo_item.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" > <ImageView android:id="@+id/iv_icon" android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_marginLeft="20dp" android:layout_toRightOf="@+id/iv_icon" android:text="应用名称" android:textColor="#000" android:textSize="16sp" /> <TextView android:id="@+id/tv_cache_size" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/tv_name" android:layout_below="@id/tv_name" android:layout_marginTop="5dp" android:text="缓存大小:" android:textColor="#000" android:textSize="16sp" /> <ImageView android:id="@+id/iv_delete" android:layout_width="45dp" android:layout_height="45dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:src="@drawable/btn_black_number_delete_selector" /> </RelativeLayout>
- 一键清理缓存
这是用的Android的一个BUG,就是你得程序去申请很大的内存,比如直接申请10G,但是你得内存总共才1G,这时候系统为了满足你得要求,会去全盘清理缓存,清理完了发现还是达不到你得要求,那么就返回失败!!!! 但是,我们的目的已经达成,就是要让他去清理全盘缓存 /** * 一键清理 * * @param view */ public void cleanAllCache(View view) { try { // 通过反射调用freeStorageAndNotify方法, 向系统申请内存 Method method = mPM.getClass().getMethod( "freeStorageAndNotify", long.class, IPackageDataObserver.class); // 参数传Long最大值, 这样可以保证系统将所有app缓存清理掉 method.invoke(mPM, Long.MAX_VALUE, new IPackageDataObserver.Stub() { @Override public void onRemoveCompleted(String packageName, boolean succeeded) throws RemoteException { System.out.println("flag==" + succeeded); System.out.println("packageName=" + packageName); } }); } catch (Exception e) { e.printStackTrace(); } }
-清理特定app缓存
查看Setting源码,分析清除缓存按钮的逻辑
实现代码:
/**
* 删除单个文件的缓存 需要权限:<uses-permission
* android:name="android.permission.DELETE_CACHE_FILES"/>
*
* @param packageName
*/
private void deleteCache(String packageName) {
try {
Method method = mPM.getClass().getMethod(
"deleteApplicationCacheFiles", String.class,
IPackageDataObserver.class);
method.invoke(mPM, packageName, new IPackageDataObserver.Stub() {
@Override
public void onRemoveCompleted(String packageName,
boolean succeeded) throws RemoteException {
System.out.println("succeeded" + succeeded);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
注意加权限: <uses-permission android:name="android.permission.DELETE_CACHE_FILES"/>
知识拓展:
加上权限后仍然报错
要想清除一个应用的缓存起作用,只需要把我们的应用变成系统应用。
如果变成系统应用?
和手机厂商合作
有root权限的手机直接安装到/system/app目录下
该功能就起作用了。
模拟器即使有root权限也装不到系统里面,因为模拟器,系统空间大小是固定的。
下面演示的时候需要用已经获得root权限的手机;
把手机卫士装到/system/app下命令:
C:\Users\Administrator>
adb push D:\workspace_test\MobileSafe\bin\MobileSafe.apk
/system/app/mobilesafe.apk
安装失败,提示系统只读
修改系统文件的权限-改权限:
执行指令:mount -o remount ,rw /system/
或者先移动到sdcard目录, 然后使用Root Explorer工具移动到system/app目录
重新安装,重启手机, 启动手机卫士,演示清除单个app的缓存逻辑
- 跳转到某个系统应用界面清除缓存
1. 看一下腾讯管家跳转到系统应用界面时的日志,并且对于Settings源代码说明意图; 2. 代码实现: //启动到某个系统应用页面 Intent intent = new Intent(); intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); intent.addCategory(Intent.CATEGORY_DEFAULT);//有无没影响 intent.setData(Uri.parse("package:"+cacheInfo.packName)); startActivity(intent);
Day12
- TabActivity的使用(知识拓展)
- 创建CleanActivity, 继承TabActivity
- 编写布局文件
<?xml version="1.0" encoding="utf-8"?> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabhost" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > //内容体 <FrameLayout android:id="@android:id/tabcontent" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" > </FrameLayout> //标签体 <TabWidget android:id="@android:id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" > </TabWidget> </LinearLayout> </TabHost>
/** * 缓存清理主页面 * * @author Kevin * */ public class CleanActivity extends TabActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_clean_base); TabHost host = getTabHost(); TabSpec tab1 = host.newTabSpec("缓存清理").setIndicator("缓存清理"); TabSpec tab2 = host.newTabSpec("SD卡清理").setIndicator("SD卡清理"); tab1.setContent(new Intent(this, CleanCacheActivity.class)); tab2.setContent(new Intent(this, CleanSdcardActivity.class)); host.addTab(tab1); host.addTab(tab2); } }
- sdcard清理
- 查看金山缓存文件夹数据库clearpath.db
- 原理介绍
1. 查询数据库中的所有缓存文件目录 2. 如果文件夹存在, 执行删除操作
- 自定义Application
- 写一个demo
/** * 自定义全局Application 应用全局的初始化逻辑可以放在此处运行 * 这是一个单例类, 整个应用只有一个 * @author Kevin */ public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); System.out.println("MyApplication onCreate"); } public void doSomething() { System.out.println("doSomething...."); } } ---------------------- public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); System.out.println("MainActivity onCreate"); MyApplication app = (MyApplication) getApplication();// 获取自定义的Application app.doSomething(); } } ---------------------- 清单文件中配置 <application android:name=".MyApplication"
- 手机卫士自定义Application
MobileSafeApplication
- 写一个demo
- 全局捕获异常
- 模拟异常, 比如1/0, 演示崩溃情况
- 代码实现
/** * 自定义全局Application * * @author Kevin * */ public class MobileSafeApplication extends Application { @Override public void onCreate() { super.onCreate(); // 设置未捕获异常处理器 Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); } class MyUncaughtExceptionHandler implements UncaughtExceptionHandler { // 未捕获的异常都会走到此方法中 // Throwable是Exception和Error的父类 @Override public void uncaughtException(Thread thread, Throwable ex) { System.out.println("产生了一个未处理的异常, 但是被哥捕获了..."); // 将异常日志输入到本地文件中, 找机会上传到服务器,供技术人员分析 File file = new File(Environment.getExternalStorageDirectory(), "error.log"); try { PrintWriter writer = new PrintWriter(file); ex.printStackTrace(writer); writer.close(); } catch (Exception e) { e.printStackTrace(); } // 结束当前进程 android.os.Process.killProcess(android.os.Process.myPid()); } } }
- 代码混淆
- 代码未混淆的前提下,打包,并进行反编译, 发现源码都可以看到, 很不安全
- 找到项目根目录下的文件project.properties, 打开混淆注释
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
- 分析文件proguard-android.txt
-
将proguard-android.txt文件拷贝到项目根目录,方便以后修改
proguard.config=proguard-android.txt:proguard-project.txt
- 重新打包并反编译,查看效果
- 结论: 混淆后,会将类名,方法名编译成a,b,c,d等混乱的字母, 提高代码阅读成本,增强安全性
-
嵌入广告
- 分析app, 广告公司, 广告平台的关系
广告平台相当于中间商, 是app和广告公司的媒介, 抽成盈利
- 盈利方式
- 展示次数, 1000次 1毛5左右 , 1分钟展示3条广告
- 点击次数, 1次 1毛5左右
- 有效点击, 1次 1元左右
- 广告公司
有米, 百度, 360, 万普, panda
- 国外广告公司
StartApp
- 项目演示
- 在StartApp上创建应用, 生成app id
- 下载SDK, 查看SDK文档
- 按照文档流程配置本地代码
- 导入jar包
- 配置清单文件
- 在主页面添加初始化代码
// 初始化广告sdk, 输入用户id和app id StartAppSDK.init(this, "103357329", "203275266", true);
-
在主页面布局文件中配置广告控件
- 在onPause和onResume中配置相关广告逻辑
- 混淆时注意跳过广告sdk验证, 因为广告sdk已经经过了混淆,再混淆的话会出错
- 分析app, 广告公司, 广告平台的关系