##day09##
- 清除来电记录
代码挂断电话后,被挂断的号码仍然会进入通话记录中, 我们需要将这种记录删除.
查看数据库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"/>
- 短信备份
- 查看短信数据库
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.xml
安装包信息在data/system/
Packages.list 里面的0 表示系统应用 1 表示用户应用
Packages.xml是存放应用的一些权限信息的;
- 判断是系统应用还是用户应用
1. 解释标识左移几位的效果public static final int FLAG_SYSTEM = 1<<0;
2. 不同标识可以加起来一起使用, 加起来的结果和特定标识进行与运算,通过计算结果可以知道具不具备该特定标识的相关功能, 这种方式叫做状态机
3. 玩游戏距离: 喝药水,加功能(加血,加攻击力,加防御,加魔法值),可以通过状态机来表示该药水具备哪些特性
4. 实际开发的机顶盒举例:{"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)"
/>
</FrameLayout>
- 清单文件中注册安装位置
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;
}
}
- 卸载,启动和分享的逻辑
/**
* 卸载
*/
private void uninstall() {
if (mCurrentAppInfo.isUserApp) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_DELETE);
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()
+ ")");
}
}
}
});