跨程序共享数据——内容提供器

本文基于书籍《Android第一行代码》


一、内容提供器简介

内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,而这种数据共享的数据来源于不同App的SQLite。内容提供器的强大在于其能够选择只对哪一部分数据进行共享,保证了隐私。

在学习内容提供器前,首先需要掌握Android6.0版本开始引入的新特性——运行时权限。


二、运行时权限

在低于Android6.0以下的版本中,App要访问一些危险权限如:查看联系人信息、拨打电话号码,只需要在Manifest文件中注册一下就可以了,但这样就导致很多App注册权限泛滥,因此Android6.0开始引入了运行时权限。

运行时权限只针对危险权限,即当App要申请危险权限时需要向用户发起申请,类似于IOS中的权限申请功能;而普通权限则由系统自动帮我们申请。Android中的危险权限共有9组24个权限。


除去上面的危险权限外,剩下的基本上都是普通权限。如果是属于这张表中的权限,那么就需要进行运行时权限处理,如果不在这张表中,那么只需要在AndroidManifest文件中添加一下权限声明即可。

但需要注意的是,当我们同意授权一个危险权限时,属于这个危险权限所在组的其他的权限也会自动授权。


那么如何申请运行时权限呢?

首先创建一个新的RuntimePermissionTest项目,修改activity_main.xml文件

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.studio.runtimepermissiontest.MainActivity">

    <Button
        android:id="@+id/make_call"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Make Call"/>

</LinearLayout>

为布局添加一个按钮,点击后拨打电话,拨打电话的权限时CALL_PHONE,属于危险权限,下面修改MainActivity中的代码

public class MainActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button makeCall= (Button) findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                //判断用户是否已经授权,未授权则向用户申请授权,已授权则直接进行呼叫操作
                if(ContextCompat.checkSelfPermission(MainActivity.this,android.Manifest.permission.CALL_PHONE)
                        != PackageManager.PERMISSION_GRANTED)
                {
                    //注意第二个参数没有双引号
                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE},1);
                }
                else
                {
                    call();
                }
            }
        });
    }

    //封装拨打电话的操作
    private void call()
    {
        try
        {
            Intent intent=new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        }
        catch (SecurityException e)
        {
            e.printStackTrace();
        }
    }

    //用户选择同意或者拒绝权限申请后回调的方法
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
    {
        switch (requestCode)
        {
            //如果同意授权则进行呼叫操作,不同意授权则提醒用户
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                {
                    call();
                }
                else
                {
                    Toast.makeText(MainActivity.this,"You denied the permission",Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }
}
在按钮点击事件中,我们构建了一个隐式Intent,Intent的action指定为Intent.ACTION_CALL,这是系统内置打电话的动作,然后在data部分指定协议为tel,号码为10086。

接下来在Manifest中注册这个权限

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.studio.runtimepermissiontest">

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

这样我们就完成了运行时权限的完整流程,下面具体分析一下。运行时权限说白了就是在程序运行的过程中由用户授权App去执行某些危险操作,App无法擅自做主。因此,第一步就是要先判断用户是否已经给过授权,这里借助的是ContextCompat.checkSelfPermission()方法。这个方法接收两个参数,第一个是Context,第二个是具体的权限名,比如打电话的权限名这就是Manifest.permission.CALL_PHONE里需要注意最好在权限名前加上android,因为Manifest.permission类下宏定义了很多权限名,但是AndroidStudio在很多情况下不会导入android包下的Manifest而是会导入这个app的Manifest,具体答案在StackOverflow上有详细的解答,网址为http://stackoverflow.com/questions/34139048/cannot-resolve-manifest-permission-access-fine-location

之后我们用这个方法的返回值和PackageManager.PERMISSION_GRANTED作比较,相等就说明用户已经授权,不等则没有授权。

如果已经授权过,那么直接执行拨打电话的操作即可,这里我们把拨打电话的逻辑封装到方法call()中。

如果没有授权过,则需要调用ActivityCompat.requestPermission()方法来向用户申请授权,这个方法接收三个参数,第一个参数是Activity的实例,第二个参数是String数组,我们把要申请的权限名放在数据中即可,第三个参数是请求码(requestCode),只要是唯一值即可,这里传入了1。

执行完requestPermission()方法后系统会弹出一个权限申请的对话框,然后用户可以选择同意或者拒绝,不论是哪种结果,最后都会回调onRequestPermissionResult()方法,授权的结果会封装在grantResults参数中。这里我们只需要判断一下最后的授权结果,如果用户同意就调用call()方法拨打电话,如果不同意就弹出一条失败提示即可。

运行App,点击Make Call按钮,之后点击同意,就会自动拨打10086了。

     

三、访问其他程序的数据

1、ContentResolver的用法

要想访问内容提供器中的数据就必要要借助ContentResolver类,我们可以通过Context类下的getContentResolver()方法获取到该类的实例对象。ContentResolver类中提供了一系列方法用于对数据的CRUD操作,如insert()、delete()、update()和query(),就方法名而言是和SQLiteDatabase是一样的。

但是不同于SQLiteDatabase,ContentResolver中的CRUD方法的参数接收的不是数据表名,而是一个Uri对象,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的App做区分的,一般采用程序包名的方式来命名,例如某个App的包名是com.studio.app,那么这个App对应的authority就是com.studio.app.provider。path则是用于对同一个App中的不同的数据表做区分的,例如某个程序的数据库里有两张表table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了com.studio.app.provider/table1和com.studio.app.provider/table2,但是一个完整标准的URI还需要一个协议声明content://,因此标准的URI为:content://com.studio.app.provider/table1和content://com.studio.app.provider/table2。

因此内容URI可以清楚地表达出我们要访问哪个App里的哪张表的数据,如果只有表名,那么我们将无法知道要访问哪个App。

在得到了内容URI字符串后,我们还需要将其解析为Uri对象才能作为参数传入,解析的方法就是Uri.parse()

Uri uri=Uri.parse("content://com.studio.app.provider/table1")
只需要调用Uri.parse()方法就可以将URI字符串解析为Uri对象。

现在我们可以使用这个Uri对象来查询table1表中的数据了,代码如下所示

Cursor cursor = getContentResolver().query(uri , projection , selection , selectionArgs , sortOrder);
这些参数和SQLiteDatabase中的query()方法里的参数很像,但要简单一些,下表对这些参数做了详细解释。


查询完返回的依然是一个Cursor对象,我们依然可以像以前一样从Cursor中取出数据,这里就不详细说明了,但要注意Cursor对象在读取完数据后要记得关闭。
下面是向表table1中添加数据的代码。
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
下面是向表table1中更新数据的代码。
ContentValues values=new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ?",new String[]{"text1" , "1"});
注意上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束,以防止所有的行都会受影响。
最后可以使用ContentResolver的delete()方法将这条数据删除掉。
getContentResolver().delete(uri,"column2 = ?",new String[]{"1"});
到这里我们就把ContentResolver类下的CRUD方法学完了,下面我们实战学习如何读取系统App电话簿里的联系人信息数据。
首先我们为模拟器添加两个联系人信息,过程直接略过,添加完毕后,新建一个ContactsTest项目,修改activity_main.xml代码,添加一个ListView。
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.studio.contactstest.MainActivity">

    <ListView
        android:id="@+id/contacts_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </ListView>

</LinearLayout>
接着修改MainActivity中的代码。
public class MainActivity extends AppCompatActivity
{

    //全局声明ListView的适配器和数据源
    ArrayAdapter<String> adapter;
    List<String> contactsList=new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView contactsView= (ListView) findViewById(R.id.contacts_view);
        //实例化适配器
        adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,contactsList);

        contactsView.setAdapter(adapter);
        //运行时权限:向用户申请访问联系人信息的权限。若用户已经同意过,则读取联系人信息;若不曾同意过,则向用户申请权限
        if(ContextCompat.checkSelfPermission(MainActivity.this,android.Manifest.permission.READ_CONTACTS)!=
                PackageManager.PERMISSION_GRANTED)
        {
            ActivityCompat.requestPermissions(MainActivity.this,new String[]{android.Manifest.permission.READ_CONTACTS},1);
        }
        else
        {
            readContacts();
        }
    }

    //读取联系人信息,适配到ListView上
    private void readContacts()
    {
        Cursor cursor=null;
        try
        {
            //查询联系人数据
            cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
            while(cursor.moveToNext())
            {
                //获取联系人姓名
                String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                //获取联系人手机号
                String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                contactsList.add(displayName + "\n" + number);
            }
            //更新适配器内容,刷新ListView
            adapter.notifyDataSetChanged();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (cursor!=null)
            {
                cursor.close();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
    {
        switch (requestCode)
        {
            case 1:
            {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                {
                    readContacts();
                }
                else
                {
                    Toast.makeText(MainActivity.this,"You denied the permission",Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
}
在onCreate()方法中,我们获取ListView控件的实例,设置好适配器,然后由于READ_CONTACTS属于危险权限,因此需要进行运行时权限处理,这里readContacts()方法是封装好的读取联系人数据并且部署在ListView上的操作逻辑。
下面继续看readContacts()方法,这里使用了ContentResolver的query()方法来查询系统App‘联系人’中的联系人数据,不过这里传入的Uri参数是ContactsContract.CommonDataKinds.Phone类帮我们封装好的URI常量,这个常量就是使用Uri.parse()方法解析出来的结果。接着遍历Cursor,联系人姓名这个字段对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,手机号字段对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER。两个数据取出后拼接,放在ListView的数据源中,刷新ListView,关闭Cursor。
最后去Menifest文件注册READ_CONTACTS权限
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.studio.contactstest">

    <uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>
...
大功告成,运行App,授予权限后效果如图。



四、创建自己的内容提供器
要想实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承ContentProvider类的方式来创建一个自定义的内容提供器,ContentProvider是一个抽象类,其下有六个抽象方法,我们在用子类继承他的时候,需要将这6个方法重写。下面新建MyProvider类继承ContentProvider类,代码如下:
public class MyProvider extends ContentProvider
{
    @Override
    public boolean onCreate()
    {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
    {
        return null;
    }

    @Nullable
    @Override
    public String getType(Uri uri)
    {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values)
    {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs)
    {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    {
        return 0;
    }
}
下面来简单介绍一下这六个重写的方法:
1、onCreate()
初始化内容提供器的时候调用,返回true表示内容提供器初始化成功,返回false表示失败。注意,只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化。
2、query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
从内容提供器中查询数据。uri参数确定查询的表,projection参数确定查询的列,selection和selectionArgs参数约束查询哪些行,sortOrder参数用于对查询结果进行排序,查询的结果存放在Cursor对象中返回
3、insert(Uri uri, ContentValues values)
向内容提供器中添加一条数据,uri参数确定添加到哪张表,要添加的数据放在values参数中,完成添加后返回一个表示新记录的uri
4、update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
更新内容提供器中已有的数据。uri参数确定更新哪一张表中的数据,values参数中存放要更新的数据,selection和selectionArgs参数约束要更新的行,完成更新后返回更新的总行数
5、delete(Uri uri, String selection, String[] selectionArgs)
从内容提供器中删除数据,uri参数确定删除哪一张表的数据,selection和selectionArgs参数约束要删除的行,完成删除后返回删除的行数。
6、getType(Uri uri)
根据传入的内容URI来确定返回相应的MIME类型。

几乎每一个方法都含有一个uri参数,这个参数正是调用ContentResolver的CRUD方法时传递过来的。现在我们要做的就是解析这个传递过来的Uri,从中分析出调用方法方期望访问的表和数据。
例如content://com.studio.app.provider/table1就表名调用方期望访问的是com.studio.app这个应用程序的内容提供器中table1表中的数据。除此之外,我们可以在这个内容URI后面加上一个id,如content://com.studio.app.provider/table1/1,这就表名要访问com.studio.app这个应用程序的内容提供器中table1表中的id为1数据行。
由此可见,内容URI以路径结尾就表示期望访问该表中的所有数据,以id结尾就表示期望访问对应id的数据。我们可以使用通配符的方式来匹配这两种格式的内容URI,规则如下:
  • * :表示匹配任意长度的任意字符
  • # :表示匹配任意长度的数字
所以,一个能够匹配任意表的内容URI格式就可以写成:content://com.studio.app.provider/*
同理,一个能够匹配表table1中任意一行数据的内容URI格式就可以写成:content://com.studio.app.provider/table1/#
之后,我们再借助UriMatcher这个类就能实现匹配内容URI的功能。UriMatcher类中包含一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样在调用UriMatcher的match()方法时,传入一个Uri对象后,返回值就是某个能够匹配这个Uri对象的自定义代码,利用这个自定义代码我们就可以分析出调用方期望访问的是哪张表的数据了。修改MainActivity中的代码,如下所示:
public class MyProvider extends ContentProvider
{
    //声明URI对应的自定义代码
    public static final int TABLE1_DIR=0;
    public static final int TABLE1_ITEM=1;
    public static final int TABLE2_DIR=2;
    public static final int TABLE2_ITEM=3;
    //声明全局UriMatcher
    private static UriMatcher uriMatcher;

    static
    {
        uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.studio.app.provider","table1",TABLE1_DIR);
        uriMatcher.addURI("com.studio.app.provider","table1/#",TABLE1_ITEM);
        uriMatcher.addURI("com.studio.app.provider","table2",TABLE2_DIR);
        uriMatcher.addURI("com.studio.app.provider","table2/#",TABLE2_ITEM);
    }

    @Override
    public boolean onCreate()
    {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
    {
        Cursor cursor=null;

        switch (uriMatcher.match(uri))
        {
            case TABLE1_DIR:
            {
                //查询table1表中的所有数据
            }
            break;
            case TABLE1_ITEM:
            {
                //查询table1表中的单条数据
            }
            break;
            case TABLE2_DIR:
            {
                //查询table2表中的所有数据
            }
            break;
            case TABLE2_ITEM:
            {
                //查询table2表中的单条数据
            }
            break;
        }

        return cursor;
    }

    @Nullable
    @Override
    public String getType(Uri uri)
    {
        switch (uriMatcher.match(uri))
        {
            case TABLE1_DIR:
            {
                return "vnd.android.cursor.dir/vnd.com.studio.app.provider.table1";
            }
            case TABLE1_ITEM:
            {
                return "vnd.android.cursor.item/vnd.com.studio.app.provider.table1";
            }
            case TABLE2_DIR:
            {
                return "vnd.android.cursor.dir/vnd.com.studio.app.provider.table2";
            }
            case TABLE2_ITEM:
            {
                return "vnd.android.cursor.item/vnd.com.studio.app.provider.table2";
            }
        }
        return null;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values)
    {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs)
    {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    {
        return 0;
    }
}
首先我们新增了四个整型常量用来表示表和表中的某条数据,这就是自定义代码。接着在静态代码块中创建UriMatcher实例,利用addUri()方法将期望匹配的内容URI格式传入。然后当调用方利用ContentResolver类下的query()方法试图访问另外app中的数据时,MyProvider类下的query()方法就会调用,利用UriMatcher类下的match()方法将传过来的Uri对象进行匹配,如果发现UriMatcher中的某个内容URI格式成功匹配到了传入的Uri对象,就会返回对应的自定义代码,这样我们就知道调用方期望访问的数据了。
上述只是一个query()方法的实例,其实insert、delete、update等方法做法也是基本一致的。
除此之外我们发现还有一个陌生的getType()方法,这个方法用于获取Uri对象对应的MIME类型,一个内容URI对应的MIME字符串主要由三部分组成:
  • 必须以vnd开头
  • 如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/
  • 最后接上vnd.<authority>.<path>
因此对于content://com.studio.app.provider/table1这个内容URI对应的MIME类型就是vnd.android.cursor.dir/vnd.com.studio.app.provider.table1
对于content://com.studio.app.provider/table1/1这个内容URI对应的MIME类型就是vnd.android.cursor.item/vnd.com.studio.app.provider.table1
到这里一个完整的内容提供器就完成了,现在任何一个应用程序都可以使用ContentResolver来访问这个应用程序中的内容。由于所有的CRUD操作都需要匹配到对应的内容URI才能进行,这样就保证了我们可以将想分享的数据才放在内容提供器中,不想分享的就不放。

下面还是实战练习一下,在之前学习持久化技术的项目DatabaseTest项目上继续进行。
首先将MyDatabaseHelper中的Toast语句删除,因为跨程序访问时不能直接使用吐司。之后创建一个内容提供器,右键包——>New——>Other——>ContentProvider,这里我们将类定义为DatabaseProvider,内容URI的authority指定为com.studio.databasetest.provider。点击Finish完成创建,之后修改其中的代码:
public class DatabaseProvider extends ContentProvider
{
    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;
    public static final String AUTHORITY="com.studio.databasetest.provider";

    private MyDatabaseHelper dbHelper;

    private static UriMatcher uriMatcher;

    static
    {
        uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
        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(Uri uri, String selection, String[] selectionArgs)
    {
        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;
        }
        return deletedRows;
    }

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

    @Override
    public Uri insert(Uri uri, ContentValues values)
    {
        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;
        }
        return uriReturn;
    }

    @Override
    public boolean onCreate()
    {
        //当ContentResolver尝试访问内容提供器时执行onCreate()方法初始化内容提供器
        dbHelper=new MyDatabaseHelper(getContext(),"BookStore.db",null,2);
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder)
    {
        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);
                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;
        }
        return cursor;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs)
    {
        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;
        }
        return updatedRows;
    }
}
虽然代码比较长但是逻辑比较简单,首先在代码一开始我们就声明了自定义代码分别表示访问Book表中的所有数据、单条数据、Category表中的所有数据、单条数据,然后在静态代码块中对UriMatcher进行了初始化操作,向其中添加了几种期望匹配的URI格式。
先看onCreate()方法:创建一个MyDatabaseHelper实例,返回true表示内容提供器初始化成功。
再看query()方法:获取SQLiteDatabse实例的同时打开(升级)数据库BookStore.db,然后根据传入的Uri参数判断用户期望访问哪张表,再调用SQLiteDabase的query()方法进行查询,并将Cursor对象返回就可以了。注意当访问单条数据的时候有一个细节,这里调用了Uri对象的getPathSegements()方法,这个方法会将URI权限(authority)之后的部分以‘/’进行分割,并把分割后的结果存放到一个字符串列表中,那么这个列表的第0个位置存放的就是路径,第一个位置存放的就是id。得到id后再通过selection和selectionArgs参数进行约束,就实现了查询的功能。
之后的方法雷同,这里不再详细解释了。

最后还有一个要注意的是,内容提供器也要在Manifest文件中注册才可以使用,不过由于我们之前使用的是AndroidStudio的快捷方式创建的内容提供器,因此已经自动注册了,打开Manifest文件:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.studio.databasetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.studio.databasetest.provider"
            android:enabled="true"
            android:exported="true">
        </provider>
    </application>

</manifest>

最后我们再新建一个项目ProviderTest来测试一下是否能够跨程序访问这个App的数据。
先修改activity_main.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.studio.providertest.MainActivity">

    <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 Book"/>

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

</LinearLayout>

然后修改MainActivity的代码
public class MainActivity extends AppCompatActivity
{

    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)
            {
                //向另一个软件DatabaseTest中添加数据
                Uri uri=Uri.parse("content://com.studio.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",55.55);
                Uri newUri=getContentResolver().insert(uri,values);
                newId=newUri.getPathSegments().get(1);
            }
        });

        Button queryData= (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener()
        {
            //从另一个软件DatabaseTest中查询数据
            @Override
            public void onClick(View v)
            {
                Uri uri=Uri.parse("content://com.studio.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
                cursor.close();
            }
        });

        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Uri uri=Uri.parse("content://com.studio.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 deleteData = (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Uri uri=Uri.parse("content://com.studio.databasetest.provider/book/"+newId);
                getContentResolver().delete(uri,null,null);
            }
        });
    }
}
我们分别在四个按钮的点击事件中处理了CRUD的逻辑,注意ContentResolver类下的insert()方法会返回一个Uri对象,这个对象中包含了新添加的数据的id,我们通过getPathSegments()方法将这个id取出,在之后的代码中有用到这个id,这就交给大家自己去琢磨了,不再详细解读,整体并不难。

最后运行程序,点击Add to Book按钮,再点Query From Book就会打印出刚刚插入的书的信息,再点Update Book,再查询就会发现书的信息发生了变化;最后点Delete From Book再点查询就发现书的信息消失了,说明成功了!














  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值