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是存放应用的一些权限信息的;
判断是系统应用还是用户应用
- 解释标识左移几位的效果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; } }
卸载,启动和分享的逻辑
/** * 卸载 */ 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() + ")"); } } } });