【Android】Android6.0读取通话记录

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

先做读取通话记录并列表显示。工程文件及分包如下:
这里写图片描述
采用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以下。

 

posted @ 2016-10-16 19:23  霍莉雪特  阅读(2552)  评论(0编辑  收藏  举报