Android中的内容提供者

Android中的内容提供者

为什么需要内容提供者

为了跨程序访问数据。试想如果在App-1中创建了一个私有数据库,App-2是不能直接访问的。因为权限不够,虽然可以使用chmod 777来修改权限,然后使用SQLiteDatabase.openDatabase的静态方法,填上具体的路径和模式来访问。但这并不推荐,有没有更好的办法?官方推荐使用ContentProvider--内容提供者。

创建内容提供者

简单起见,使用以前的数据库的项目DatabaseTest,同时建立两个表book和category, onUpgrade方法实现了数据库的升级功能。onUpgrade里面强制onCreate,注意必须先删除原来的表,否则我们创建时候发现原来的表还存在就会报错。

package com.example.administrator.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;


public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text)";

    public static final String CREATE_CATEGORY = "create table category ("
            + "id integer primary key autoincrement, "
            + "category_name text, "
            + "category_code integer)";

    private Context mContext;

    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        this.mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists book");
        db.execSQL("drop table if exists category");
        onCreate(db);
    }
}

MainActivity里面就显示下界面,省略了。

如果想让这个数据库共享,其他应用也能访问?只需新增一个内容提供者即可。

New -> Other -> ContentProvider,AS会帮我们在AndroidManifest.xml里注册好。有一个属性authorities比较重要,一般命名方式是<包名>.provider,比如com.example.cptest.provider

可以看到,内容提供者的方法和操作数据库差不多。最大的不同是操作数据库需要填上表名,而内容提供者中的方法需要填上Uri。为什么呢?因为是跨程序访问数据,多个应用的表名可能一样,这样就不知道到底访问哪个应用的数据了。Uri的格式一般如下

content://<package_name>.provider/<path>/<id>,举个例子content://com.example.databasetest.provider/book/2表示访问book表的id为2的那行数据。

甚至可以使用通配符

  • *表示匹配任意长度的任意字符
  • #表示匹配任意长度的数字

于是可以匹配任意表的URI可以写成content://com.example.databasetest.provider/*

可以匹配一个表中任意一行的URI可以写成content://com.example.databasetest.provider/book/#

package com.example.administrator.databasetest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.NonNull;

public class DatabaseProvider extends ContentProvider {
    // 0123是自定义代码,用于清楚表达我们想要访问访问数据库的哪个表或者哪一行数据
    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM = 3;
    // 和清单文件里provider的authority属性一致
    public static final String AUTHORITY = "com.example.databasetest.provider";

    private static UriMatcher uriMatcher;

    private MyDatabaseHelper dbHelper;

    static {
      // NO_MATCH就是-1
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
      // 第三个参数填上自定义的代码,对应于uriMatcher.match(uri)
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
        // Implement this to handle requests to delete one or more rows.
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                deletedRows = db.delete("book", selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deletedRows = db.delete("book", "id = ?", new String[]{bookId});
                break;
            case CATEGORY_DIR:
                deletedRows = db.delete("category", selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deletedRows = db.delete("category", "id = ?", new String[]{categoryId});
                break;
            default:
        }
        return deletedRows;
    }

    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.administrator.databasetest.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.administrator.databasetest.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.administrator.databasetest.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.administrator.databasetest.provider.category";
            default:
                return null;
        }
    }

    @Override
    public Uri insert(@NonNull Uri uri, ContentValues values) {
        // TODO: Implement this to handle requests to insert a new row.
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("book", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("category", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
        }
        return uriReturn;
    }
    // 一旦使用到内容提供者就调用此方法,并得到数据库连接的实例,返回true表示内容提供者初始化成功
    @Override
    public boolean onCreate() {
        // TODO: Implement this to initialize your content provider on startup.
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 19);
        return true;
    }

    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // TODO: Implement this to handle query requests from clients.
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                cursor = db.query("book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1); // 这里path是/book/bookId,get(1)就是bookId
                cursor = db.query("book", projection, "id = ?", new String[]{bookId}, null, null, sortOrder);
                break;
            case CATEGORY_DIR:
                cursor = db.query("category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("category", projection, "id = ?", new String[]{categoryId}, null, null, sortOrder);
                break;
            default:
        }
        return cursor;
    }

    @Override
    public int update(@NonNull Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // TODO: Implement this to handle requests to update one or more rows.
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updatedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                updatedRows = db.update("book", values, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                updatedRows = db.update("book", values, "id = ?", new String[]{bookId});
                break;
            case CATEGORY_DIR:
                updatedRows = db.update("category", values, selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updatedRows = db.update("category", values, "id = ?", new String[]{categoryId});
                break;
            default:
        }
        return updatedRows;
    }
}

流程是这样的,一旦需要内容提供者时就会调用其onCreate方法并且实例化了数据库连接帮助类。提供了UriMatcher,在静态代码块里初始化,添加上我们期望匹配的URI

 static {
      // NO_MATCH就是-1
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
      // 第三个参数填上自定义的代码,对应于uriMatcher.match(uri)
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

接收三个参数,分别是authority、path和自定义唯一码。

增删改查的方法就不说了,注意两点。

  1. 有个新方法uri.getPathSegments().get(1);这是什么意思呢?简单来说比如一个URI是这样的content://com.example.databasetest.provider/book/2,那么以provider/处分割,后面的部分是<path>.<id>,那么get(0)就获取到了路径,get(1)就获取到了id。
  2. insert方法返回的是一个新的URI,比如新增的一行db.insert返回一个新的id为3,那么内容提供者的insert方法返回的新URI为content://com.example.databasetest.provider/book/3

最后介绍getType()这个方法 -- 根据传入的内同URI来返回相应的MIME类型。

  • 必须以vnd开头
  • 如果URI以path结尾,则后接android.cursor.dir/;如果URI以id结尾,则后接android.cursor.item/
  • 最后接上vnd.<authority>.<path>

对于content://com.example.databasetest.provider/book这个URI,对应的MIME是vnd.android.cursor.dir/vnd.com.example.databasetest.book;

对于content://com.example.databasetest.provider/book/2这个URI,对应的MIME是vnd.android.cursor.item/vnd.com.example.databasetest.book

好了内容提供者写好了,赶紧在另外一个应用里尝试一下!

通过内容提供者访问数据

假设此应用时App-B,上面的应用是App-A。

布局实现对上述应用数据库中的book表的CURD

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >


    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add to Book" />

    <Button
        android:id="@+id/query_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query From Book" />

    <Button
    android:id="@+id/update_data"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Update Data" />

    <Button
        android:id="@+id/del_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete Data" />

</LinearLayout>

MainActivity,所有的方法都是基于getContentResolver()。得到内容提供者后,尝试访问App-A的数据。此时App-A里内容提供者的onCreate方法得到执行,由此创建了数据库。

我们只需正确匹配Uri就能访问到App-A中的数据库了,代码很简单,不需要讲解了。

package com.sunhaiyu.contentprovidertest;

import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    // newId是每插入一条数据就会被赋值的,所以进行更新和删除操作时只能操作最后插入的数据,其他数据不会受到影响
    private String newId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
                ContentValues values = new ContentValues();
                values.put("name", "A Clash of Kings");
                values.put("author", "George Martin");
                values.put("pages", 1040);
                values.put("price", 19.99);
                Uri newUri = getContentResolver().insert(uri, values);
                if (newUri != null) {
                    newId = newUri.getPathSegments().get(1);
                }
            }
        });

        Button queryData = (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
                Cursor cursor = getContentResolver().query(uri, null, null, null, null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));

                        Log.d("MainActivity", "book name is " + name);
                        Log.d("MainActivity", "book author is " + author);
                        Log.d("MainActivity", "book pages is " + pages);
                        Log.d("MainActivity", "book price is " + price);
                    }
                    cursor.close();
                }
            }
        });

        Button  updataData = (Button) findViewById(R.id.update_data);
        updataData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
                ContentValues values = new ContentValues();
                values.put("name", "A Storm of Swords");
                values.put("pages", 1216);
                values.put("price", 24.05);
                getContentResolver().update(uri, values, null, null);
            }
        });

        Button delData = (Button) findViewById(R.id.del_data);
        delData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
                getContentResolver().delete(uri, null, null);
            }
        });
    }
}

是不是很方便?使用ContentProvider就能式样App-A轻松访问到App-B中的内容。还有一些常见的例子,比如访问联系人和短信数据等。

短信的备份

由于短信的数据库已经通过内容提供者暴露出来 所以我们直接通过内容的解析者去查询数据库,查看源码其authority是sms;查看添加的URI,其中有一条null,表示读取全部短信。

备份好的xml大概长这样

<Smss>
    <Sms>
        <address>123456</number>
        <date>"2017/4/10"</date>
        <body>"请你吃饭,快出来!给你5秒 5"</body>
    </Sms>
    <Sms>
        <address>123456</number>
        <date>"2017/2/10"</date>
        <body>"请你吃饭,快出来!给你5秒 4"</body>
    </Sms>
</Smss>

布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context="com.sunhaiyu.smscontentprovider.MainActivity">

    <Button
        android:id="@+id/bt_backup_sms"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="备份短信" />

    <Button
        android:id="@+id/bt_restore_sms"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="恢复短信" />

</LinearLayout>

MainActivity

package com.sunhaiyu.smscontentprovider;

import android.Manifest;
import android.support.v7.app.AppCompatActivity;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Xml;
import android.view.View;
import android.widget.Button;

import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private List<String> permissons = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            permissons.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }
        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
            permissons.add(Manifest.permission.READ_SMS);
        }
        if (!permissons.isEmpty()) {
            ActivityCompat.requestPermissions(MainActivity.this, permissons.toArray(new String[permissons.size()]), 1);

        }
        Button btBackup = (Button) findViewById(R.id.bt_backup_sms);
        Button btRestore = (Button) findViewById(R.id.bt_restore_sms);
        btBackup.setOnClickListener(this);
        btRestore.setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt_backup_sms:
                // 点击按钮查询短信内容 然后把短信内容进行备份
                try {
                    //[1]获取XmlSerializer的实例
                    XmlSerializer serializer = Xml.newSerializer();
                    //[2]设置序列化器参数
                    File file = new File(Environment.getExternalStorageDirectory().getPath(), "smsbackup.xml");
                    FileOutputStream fos = new FileOutputStream(file);
                    serializer.setOutput(fos, "utf-8");
                    //[3]写xml文档开头, 第二个参数,是否独立,xml默认独立,填写true
                    serializer.startDocument("utf-8", true);

                    //[4]写xml的根节点
                    serializer.startTag(null, "smss");
                    //[5]构造uri,这个authority为sms从源码可以看到,同时path为null表示查询所有的短信,所以URI就是下面的样子了
                    Uri uri = Uri.parse("content://sms/"); // content://sms 也可以

                    //[6]由于短信的数据库已经通过内容提供者暴露出来 所以我们直接通过内容解析者查询
                    Cursor cursor = getContentResolver().query(uri, new String[]{"address", "date", "body"}, null, null, null);

                    if (cursor != null) {
                        while (cursor.moveToNext()) {
                            String address = cursor.getString(cursor.getColumnIndex("address"));
                            String date = cursor.getString(cursor.getColumnIndex("date"));
                            String body = cursor.getString(cursor.getColumnIndex("body"));

                            //[7]写sms节点
                            serializer.startTag(null, "sms");

                            //[8]写address节点
                            serializer.startTag(null, "address");
                            serializer.text(address);
                            serializer.endTag(null, "address");
                            //[9]写date节点
                            serializer.startTag(null, "date");
                            serializer.text(date);
                            serializer.endTag(null, "date");
                            //[10]写body节点
                            serializer.startTag(null, "body");
                            serializer.text(body);
                            serializer.endTag(null, "body");

                            serializer.endTag(null, "sms");

                        }
                        cursor.close();
                    }

                    serializer.endTag(null, "smss");
                    serializer.endDocument();

                } catch (Exception e) {
                    e.printStackTrace();
                }

                break;
            case R.id.bt_restore_sms:
                break;
            default:
                break;
        }
    }
}

由于我直接八备份文件放在了sd卡根目录下,所以需要申请权限。而且读取短信也需要权限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_SMS" />

之后再申请运行时权限。

没有实现短信恢复的功能,这个功能现在实现起来麻烦了。

不能直接申请WRITE_SMSA权限了!Android 4.4 (KitKat) 开始,更新了 SMS 的部分API。只有default SMS app才能对短信数据库有写权限,但是用户可以把第三方应用设置为default SMS app。详情看这里

其实想想也正常,如果任何应用都能写入短信数据库,将是一大安全隐患。

读取手机联系人

手机联系人信息也通过provider对外暴露。任何应用都可以轻松访问到。ContactsContract。CommonDataKinds.Phone这个类已经帮我们封装好了,使得用户不用操心URI的匹配问题,十分方便。

package com.sunhaiyu.contacttest;

import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_CONTACTS}, 1);
        }

        Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                String phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                Log.d("Contact", displayName + " : " + phoneNumber);
            }
            cursor.close();
        }
    }
}

记得添加权限<uses-permission android:name="android.permission.READ_CONTACTS"/>

插入联系人

插入联系人也很方便,使用方式和上面大同小异。

首先是布局,输入姓名和手机号码,点击按钮就可插入到联系人。

<LinearLayout 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"
   tools:context=".MainActivity" >

   <EditText
       android:id="@+id/et_name"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:hint="请输入姓名" />

   <EditText
       android:id="@+id/et_phone"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:hint="请输入电话号码" />

   <Button
       android:id="@+id/bt_add_contact"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="插入到联系人" />

</LinearLayout>

然后关键是addContact方法了,注意先插入空值以获得一个新的id,之后使用这个ID添加联系人姓名和手机好,下面的代码可以说是一个模板。

package com.sunhaiyu.addcontacttest;

import android.Manifest;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.ContactsContract;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private EditText etName;
    private EditText etPhone;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_CONTACTS}, 1);
        }

        etName = (EditText) findViewById(R.id.et_name);
        etPhone = (EditText) findViewById(R.id.et_phone);
        Button btAdd = (Button) findViewById(R.id.bt_add_contact);
        btAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String name = etName.getText().toString().trim();
                String phone = etPhone.getText().toString().trim();
                addContact(name, phone);
            }
        });

    }

    public void addContact(String name, String phoneNumber) {
        // 创建一个空的ContentValues
        ContentValues values = new ContentValues();

        // 向RawContacts.CONTENT_URI空值插入,用于获取Android系统返回的rawContactId。后面要基于此id插入值
        Uri rawContactUri = getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);
        long rawContactId = ContentUris.parseId(rawContactUri);
        // 添加下一条之前先清空
        values.clear();
        // 1. 添加联系人姓名
        // id
        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
        // 内容类型添加了才会显示出来,否则显示无姓名
        values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
        // 联系人名字
        values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, name);
        // 向联系人URI添加联系人名字
        getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);


        values.clear();
        // 2. 添加手机号码,这个id要保证和上面的一致
        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
        values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
        // 联系人的电话号码
        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
        // 电话类型
        values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
        // 向联系人电话号码URI添加电话号码
        getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);

        Toast.makeText(this, "联系人数据添加成功", Toast.LENGTH_SHORT).show();
    }

}

记得添加权限<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

内容观察者简介

当某一个应用中内容提供者中共享的数据发生改变时候,就会收到一个通知。具体来说,当App-A访问或者修改了内容提供者的数据时,同时发送一个通知。(调用了getContentResolver().notifyChange())然后App-B中会响应onChange()方法。起到一个监视的作用。

一个简单的例子,监听短信数据库的变化。系统源码中已经发送了通知,我们只需接受即可。

package com.sunhaiyu.contentbservertest;

import android.Manifest;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_SMS)!= PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_SMS}, 1);
        }
        //[1]注册一个内容观察者
        Uri uri = Uri.parse("content://sms/");
        // false表示指定的这个URI和其父路径 true还能表示其子路径
        getContentResolver().registerContentObserver(uri, true, new MyContentObserver(new Handler()));

    }

    private class MyContentObserver extends ContentObserver{

        public MyContentObserver(Handler handler) {
            super(handler);
        }
        //当观察的内容发生改变的时候调用
        @Override
        public void onChange(boolean selfChange) {
            Toast.makeText(MainActivity.this, "短信数据库变化", Toast.LENGTH_SHORT).show();
            Log.d("Sms", "onChange: ");
            super.onChange(selfChange);
        }

    }
}

记得添加权限<uses-permission android:name="android.permission.READ_SMS" /


by @sunhaiyu

2017.6.5

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容