需求:读取通话记录,然后列表显示,每条记录的数据包括姓名、号码、类型(来电、去电、未接,字体颜色分别为绿、蓝、红),然后长按条目弹出一个列表弹窗,显示【复制号码到拨号盘】、【发短信】、【打电话】。

先做读取通话记录并列表显示。工程文件及分包如下:

采用MVC模式:CallInfo数据模型,CallInfoService负责获取数据,MainActivity负责显示。

CallInfo数据模型:包含字段姓名、号码、类型。

public class CallInfo {

    public String number; // 号码
public long date; // 日期
public int type; // 类型:来电、去电、未接 public CallInfo(String number, long date, int type) {
this.number = number;
this.date = date;
this.type = type;
} @Override
public String toString() {
return "CallInfo{" +
"number='" + number + '\'' +
", date=" + date +
", type=" + type +
'}';
}
}

CallInfoService类获取通话记录数据。

public class CallInfoService {

    /**
* 获取通话记录
* @param context 上下文。通话记录需要从系统的【通话应用】中的内容提供者中获取,内容提供者需要上下文。通话记录保存在联系人数据库中:data/data/com.android.provider.contacts/databases/contacts2.db库中的calls表。
* @return 包含所有通话记录的一个集合
*/
public static List<CallInfo> getCallInfos(Context context) {
List<CallInfo> infos = new ArrayList<CallInfo>();
ContentResolver resolver = context.getContentResolver();
// uri的写法需要查看源码JB\packages\providers\ContactsProvider\AndroidManifest.xml中内容提供者的授权
// 从清单文件可知该提供者是CallLogProvider,且通话记录相关操作被封装到了Calls类中
Uri uri = CallLog.Calls.CONTENT_URI;
String[] projection = new String[]{
CallLog.Calls.NUMBER, // 号码
CallLog.Calls.DATE, // 日期
CallLog.Calls.TYPE // 类型:来电、去电、未接
}; Cursor cursor = resolver.query(uri, projection, null, null, null);
while (cursor.moveToNext()){
String number = cursor.getString(0);
long date = cursor.getLong(1);
int type = cursor.getInt(2);
infos.add(new CallInfo(number, date, type));
}
cursor.close();
return infos;
}
}

MainActivity负责显示:

public class MainActivity extends AppCompatActivity {

    private MyAdapter adapter;

    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 条目显示
ListView lv = (ListView) findViewById(R.id.lv);
List<CallInfo> infos = CallInfoService.getCallInfos(this);
adapter = new MyAdapter(infos);
lv.setAdapter(adapter);
} private class MyAdapter extends BaseAdapter{ private List<CallInfo> infos;
private LayoutInflater mInflater; public MyAdapter(List<CallInfo> infos) {
super();
this.infos = infos;
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
} @Override
public int getCount() {
return infos.size();
} @Override
public Object getItem(int position) {
return infos.get(position);
} @Override
public long getItemId(int position) {
return 0;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
// 加载布局
View view = mInflater.inflate(R.layout.calllog_item, null);
// 获取控件
TextView tv_number = (TextView) view.findViewById(R.id.tv_number);
TextView tv_date = (TextView) view.findViewById(R.id.tv_date);
TextView tv_type = (TextView) view.findViewById(R.id.tv_type);
// 设置控件内容
CallInfo info = infos.get(position);
// 号码
tv_number.setText(info.number);
// 日期
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String dateString = format.format(info.date);
tv_date.setText(dateString);
// 类型
String type = null;
int textColor = 0;
switch (info.type){
case CallLog.Calls.INCOMING_TYPE: // 来电,字体蓝色
type = "来电";
textColor = Color.BLUE;
break;
case CallLog.Calls.OUTGOING_TYPE: // 去电,字体绿色
type = "去电";
textColor = Color.GREEN;
break;
case CallLog.Calls.MISSED_TYPE: // 未接,字体红色
type = "未接";
textColor = Color.RED;
break;
}
tv_type.setText(type);
tv_type.setTextColor(textColor); return view;
}
}
}

AndroidManifest.xml清单文件需要添加读(写)通话记录的权限:

<!-- 读写通话记录 -->
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" /><!-- 低版本使用该权限 -->
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" /><!-- 为了兼容低版本 -->

写完以上部分进行测试,在Android4.4.3真机上可正常运行。启动时系统会弹窗提示该应用正在尝试读取通话记录,是否允许。在Android6.0模拟器上测试报错安全异常:

java.lang.SecurityException: Permission Denial
...
requires android.permission.READ_CALL_LOG or android.permission.WRITE_CALL_LOG

权限被拒绝,需要READ_CALL_LOG或WRITE_CALL_LOG权限。是的,即使在清单文件中已经声明了该权限,依然报错没有该权限,推断这个问题依然是Android6.0的运行时权限问题。

搜索一下看到如下资料:
http://www.javahelps.com/2015/10/android-60-runtime-permission-model.html

上文的step8提到了这一点:

解决方法还是参考官方文档:
https://developer.android.com/training/permissions/requesting.html#perm-check

所以要将MainActivity和CallInfoService的代码修改,加上长按条目弹出列表弹窗的功能后,代码如下:

MainActivity:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
private MyAdapter adapter;
private ListView lv; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 获取条目显示的控件
lv = (ListView) findViewById(R.id.lv);
// 尝试获所有需要的授权
CallInfoService.getPermissions(this);
} @Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case CallInfoService.MY_PERMISSIONS_REQUESTS:
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 授权成功,开始获取通话记录
Log.i(TAG, "所需权限授权成功!");
List<CallInfo> infos = CallInfoService.getCallInfo(this); // 显示条目
adapter = new MyAdapter(infos);
lv.setAdapter(adapter); // 条目设置长按事件,弹出一个列表对话框
lv.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
// 获取条目对应的号码
CallInfo info = (CallInfo) adapter.getItem(position);
final String number = info.number; String[] items = new String[]{
"复制号码到拨号盘",
"拨号",
"发送短信"
};
new AlertDialog.Builder(MainActivity.this)
.setTitle("操作")
.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
// 复制号码到拨号盘
startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number)));
break;
case 1:
// 拨号
if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "没有授权拨号!");
return;
}
startActivity(new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number)));
break;
case 2:
// 发送短信
if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "没有授权发短信!");
return;
}
startActivity(new Intent(Intent.ACTION_SENDTO, Uri.parse("sms:" + number)));
break; }
}
}).show();
return false;
}
});
} else {
Log.i(TAG, "所需权限授权失败!");
}
break;
default:
break;
}
} private class MyAdapter extends BaseAdapter{ private List<CallInfo> infos;
private LayoutInflater mInflater; public MyAdapter(List<CallInfo> infos) {
super();
this.infos = infos;
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
} @Override
public int getCount() {
return infos.size();
} @Override
public Object getItem(int position) {
return infos.get(position);
} @Override
public long getItemId(int position) {
return 0;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
// 加载布局
View view = mInflater.inflate(R.layout.calllog_item, null);
// 获取控件
TextView tv_number = (TextView) view.findViewById(R.id.tv_number);
TextView tv_date = (TextView) view.findViewById(R.id.tv_date);
TextView tv_type = (TextView) view.findViewById(R.id.tv_type);
// 设置控件内容
CallInfo info = infos.get(position);
// 号码
tv_number.setText(info.number);
// 日期
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String dateString = format.format(info.date);
tv_date.setText(dateString);
// 类型
String type = null;
int textColor = 0;
switch (info.type){
case CallLog.Calls.INCOMING_TYPE: // 来电,字体蓝色
type = "来电";
textColor = Color.BLUE;
break;
case CallLog.Calls.OUTGOING_TYPE: // 去电,字体绿色
type = "去电";
textColor = Color.GREEN;
break;
case CallLog.Calls.MISSED_TYPE: // 未接,字体红色
type = "未接";
textColor = Color.RED;
break;
}
tv_type.setText(type);
tv_type.setTextColor(textColor); return view;
}
} }

CallInfoService:

public class CallInfoService {

    private static final String TAG = "CallInfoService";
private static String[] permissionList = new String[]{
Manifest.permission.READ_CALL_LOG,
Manifest.permission.CALL_PHONE,
Manifest.permission.SEND_SMS
}; public static final int MY_PERMISSIONS_REQUESTS = 0; // 批量申请多个权限:读取通话记录、打电话、发短信 /**
* 获取读取通话记录、打电话、发短信的权限
* @param activity 用于弹窗申请权限的Activity
*/
public static void getPermissions(Activity activity) {
ArrayList<String> list = new ArrayList<String>();
// 循环判断所需权限中有哪个尚未被授权
for (int i = 0; i < permissionList.length; i++){
if (ActivityCompat.checkSelfPermission(activity, permissionList[i]) != PackageManager.PERMISSION_GRANTED)
list.add(permissionList[i]);
} ActivityCompat.requestPermissions(activity, list.toArray(new String[list.size()]), MY_PERMISSIONS_REQUESTS);
} /**
* 请求获取通话记录
* @param context 上下文。通话记录需要从系统的【通话应用】中的内容提供者中获取,内容提供者需要上下文。
* 通话记录保存在联系人数据库中:data/data/com.android.provider.contacts/databases/contacts2.db库中的calls表。
* @return 一个包含所有通话记录的集合。
*/
public static List<CallInfo> getCallInfo(Context context) {
List<CallInfo> infos = new ArrayList<CallInfo>();
ContentResolver resolver = context.getContentResolver();
// uri的写法需要查看源码JB\packages\providers\ContactsProvider\AndroidManifest.xml中内容提供者的授权
// 从清单文件可知该提供者是CallLogProvider,且通话记录相关操作被封装到了Calls类中
Uri uri = CallLog.Calls.CONTENT_URI;
String[] projection = new String[]{
CallLog.Calls.NUMBER, // 号码
CallLog.Calls.DATE, // 日期
CallLog.Calls.TYPE // 类型:来电、去电、未接
}; if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "授权失败,无法获取通话记录!");
return null;
}
Cursor cursor = resolver.query(uri, projection, null, null, null);
while (cursor.moveToNext()){
String number = cursor.getString(0);
long date = cursor.getLong(1);
int type = cursor.getInt(2);
infos.add(new CallInfo(number, date, type));
}
cursor.close(); return infos;
} }

这里是一次批量申请多个权限,写法比较蛋疼,涉及List集合和Stirng[]相互转换,不懂哪位大佬有更好的批量申请写法,这里暂时就这么写了。

最后在Android6.0模拟器上测试成功,效果如下图:



然而!!!!!!

坑爹的情况又出现了,以上代码采用Android6.0的运行时权限的模式编写,可以在Android6.0的模拟器上正常运行,但是在Android5.1.1的真机上测试出了些问题:
通过打Log发现是SEND_SMS权限没有获得授权,然后在调用ActivityCompat.requestPermissions()去申请权限时并没有任何弹窗,直接就返回申请失败了。

好吧,再次搜索资料,找到了重要参考

http://www.jianshu.com/p/e1ab1a179fbb
http://blog.csdn.net/hudashi/article/details/50775180

综上,解决方案大致有以下几种:

  • 直接简单粗暴地判断是否Build.VERSION.SDK_INT >= 23,然后分高低版本两种逻辑处理。
  • 或者用兼容库使代码兼容旧版,如v4兼容库。
  • 或者使用第三方库简化代码,如Github上的开源项目 PermissionHelper和hotchemi’s PermissionsDispatcher

好吧,看来还需要再花些时间整理一下,未完待续。。。


小结:

  • 如果已经在清单文件中声明了权限,依然报错安全异常权限被拒绝,多半是因为这是个运行时权限。
  • 现在Android Studio创建项目时默认是targetSdkVersion 24,如果暂不打算兼容高版本,需要在build.gradle修改targetSdkVersion至23以下。

【Android】Android6.0读取通话记录的更多相关文章

  1. Android笔记——对系统通话记录的删除操作

    手机通话记录是保存在数据库中的,位置:  /data/data/com.android.providers.contacts/databases/calllog.db ,表名:calls 这张表中有个 ...

  2. Android 6.0 双向通话自动录音

    package com.example.hgx.phoneinfo60.Recording; import android.content.BroadcastReceiver; import andr ...

  3. android6.0获取通讯录权限

    android6.0中,获取通讯录的权限是    <uses-permission android:name="android.permission.GET_ACCOUNTS" ...

  4. 玩下软工项目,第一轮--全局Context的获取,SQLite的建立与增删改查,读取用户通话记录信息

    项目的Github地址:https://github.com/ggrcwxh/LastTime 采用基于git的多人协作开发模式 软件采用mvc设计模式,前端这么艺术的事我不太懂,交给斌豪同学去头疼了 ...

  5. Android-读取操作系统通话记录并/拨打电话/发送短信/复制号码到拨号盘

    apps目录的contacts应用(有读取通话记录功能),是访问provider目录的provider.contacts应用(有暴露通话记录),所以要阅读Android操作系统源码-->pack ...

  6. Android WiFi热点完全研究(自定义创建、跳转系统界面设置、读取配置、切换,Android6.0适配)

    前言: WiFi热点设置页面的安全性选项在Android 4.x上有“无”.“WPA PSK”.“WPA2 PSK”三个选项,在Android 5.0(含)之后去掉了WPA PSK选项(部分手机厂家会 ...

  7. android 获取通话记录

    在manifest添加以下权限<uses-permission android:name="android.permission.READ_CALL_LOG" />&l ...

  8. 建立一个类似于天眼的Android应用程序:第4部分 - 持久收集联系人,通话记录和短信(SMS)

    建立一个类似于天眼的Android应用程序:第4部分 - 持久收集联系人,通话记录和短信(SMS) 电话黑客android恶意软件编程黑客入侵linux 随着我们继续我们的系列,AMUNET应用程序变 ...

  9. Android项目的targetSDK>=23,在低于Android6.0的部分测试机(类似华为)上运行时出现的系统权限问题

    相信大家对Android6.0以上的动态权限已经有所了解,很多童鞋也已经跃跃欲试地将自己项目的targetSDK升级到了23及其以上,很不幸的是我也成为了其中一员,然而我还是图样图森破了,升级之后的问 ...

随机推荐

  1. 微信图片分享遇到 checkArgs fail, thumbData is invalid

    该问题主要是微信图片限制32K以内的原因,可将bmpToByteArray方法进行进行改写. 原方法是: /** * 得到Bitmap的byte * @author netcorner * @para ...

  2. SQLServer2008 全文检索摘记

    最近在做全文搜索的内容,google了一下全文检索,发现了一些问题,现在总结如下: 全文索引和查询概念(摘自SQL 联机帮助)SQL Server 2008 为应用程序和用户提供了对 SQL Serv ...

  3. iOS 9 学习系列: Xcode Code Coverage

    Code coverage 是一个计算你的单元測试覆盖率的工具. 高水平的覆盖给你的单元測试带来信心.也表明你的应用被彻底的測试过了. 你可能写了几千个单元測试,但假设覆盖率不高.那么你写的这套測试可 ...

  4. Linux查看文件夹占用空间

    du -sh *   查看当前目录所有文件的各个大小/home/econf>du -sh *427M apache-tomcat-6.0.2016K bin44M boot6.7M filese ...

  5. 【转载并整理】Linux - centOS 6 SVN服务器安装、配置及开机启动

    以前在windows上安装svn服务器,用的是VisualSVN-Server用起来还不错,但只能用在windows,在linux上部署使用是 subversion,具体说明如下 参考原文:http: ...

  6. linux 压缩和解压命令

    Linux下的压缩解压缩命令详解及实例 实例:压缩服务器上当前目录的内容为xxx.zip文件 zip -r xxx.zip ./* 解压zip文件到当前目录 unzip filename.zip == ...

  7. Vue style 深度作用选择器 >>> 与 /deep/(sass/less)

    传送门:https://vue-loader-v14.vuejs.org/zh-cn/features/scoped-css.html 你很可能会遇到的问题 vue组件编译后,会将 template ...

  8. MySql计算两个日期的时间差函数

    MySql计算两个日期时间的差函数: 第一种:TIMESTAMPDIFF函数,需要传入三个参数,第一个是比较的类型,可以比较FRAC_SECOND.SECOND. MINUTE. HOUR. DAY. ...

  9. HTTP报文头Accept和Content-Type总结

    1.Accept属于请求头, Content-Type属于实体头. Http报头分为通用报头,请求报头,响应报头和实体报头. 请求方的http报头结构:通用报头|请求报头|实体报头 响应方的http报 ...

  10. XMPP 安装ejabberd 搭建服务器环境

    网上各种找..各种安装失败.. 终于.... ejabberd 下载列表.... http://www.process-one.net/en/ejabberd/archive/  建议下载old 版本 ...