8月5日 第7章跨程序共享数据——探究内容提供器

1.权限

普通权限申请系统会默认同意,危险权限需要用户手动同意授权

危险权限表:

表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名。原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权。但是,不要基于此规则来实现任何功能逻辑,因为Android系统随时有可能调整权限的分组。

访 问 http://developer.android.google.cn/reference/android/Manifest.permission.html 可以查看 Android 系统中完整的权限列表。

2.在程序运行时申请权限

新建一个 RuntimePermissionTest 项目

添加一个按钮,用来拨打10086

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="10086"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

按钮逻辑

Button makeCall = findViewById(R.id.button);
    makeCall.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (ContextCompat.checkSelfPermission(MainActivity.this, 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(this, "操作失败", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

点击按钮后进行判断

ContextCompat.checkSelfPermission()方法接收两个参数, 第一个参数是 Context,第二个参数是具体的权限名。然后我们使用方法的返回值(PackageManager.PERMISSION_GRANTEDPackageManager.PERMISSION_DENIED)和 PackageManager. PERMISSION_GRANTED (获得该权限)做比较,相等就说明用户已经授权,不等就表示用户没有授权

我们把拨打电话的逻辑封装到了 call()方法当中,如果用户授权了直接进行拨打电话

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

调用完了 requestPermissions()方法之后,系统会弹出一个权限申请的对话框,然后用户可以选择同意或拒绝我们的权限申请,用户选择之后会调用到 onRequestPermissionsResult()方法,而授权的结果则会封装在 grantResults 参数中。这里我们只需要判断一下最后的授权结果,如果用户同意的话就调用call()方法来拨打电话,如果用户拒绝的话我们只能放弃操作,并且弹出失败提示。

3.访问其他程序中的数据

1.ContentResolver 的基本用法

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助 ContentResolver 类,可以通过 Context 中的 getContentResolver()方法获取到该类的实例。

ContentResolver 中提供了一系列的方法用于对数据进行 CRUD 操作,其中 insert()方法用于添加数据, update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据

不同于 SQLiteDatabase,ContentResolver 中的增删改查方法都是不接收表名参数的,而是使 用一个 Uri 参数代替,这个参数被称为内容 URI。

内容 URI 给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority 和 path。

authority 是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。

path 则是用于对同一 应用程序中不同的表做区分的,通常都会添加到 authority 的后面。

比如内容 URI 变成 com.example.app.provider/table1 或com.example.app.provider/table2

目前还很难辨认出这两个字符串就是两个内容 URI,我们还需要在字符串的头部加上协议声明。因此,内容 URI 最标准的格式写法如下: content://com.example.app.provider/table1 content://com.example.app.provider/table2

在得到了内容 URI 字符串之后,我们还需要将它解析成 Uri 对象才可以作为参数传入。解 析的方法也相当简单,代码如下所示: Uri uri = Uri.parse("content://com.example.app.provider/table1") 只需要调用 Uri.parse()方法,就可以将内容 URI 字符串解析成 Uri 对象了。

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

<ListView
    android:id="@+id/contacts_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:ignore="MissingConstraints"
    />

这些参数和 SQLiteDatabase 中 query()方法里的参数很像,但总体来说要简单一些,毕竟这 是在访问其他程序中的数据,没必要构建过于复杂的查询语句。下表对使用到的这部分参数进行 了详细的解释。

查询完成后返回的仍然是一个 Cursor 对象,这时我们就可以将数据从 Cursor 对象中逐个 读取出来了。读取的思路仍然是通过移动游标的位置来遍历 Cursor 的所有行,然后再取出每一 行中相应列的数据,代码如下所示:

if (cursor != null) {      
    while (cursor.moveToNext()) {   
        String column1 = cursor.getString(cursor.getColumnIndex("column1"));   
        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));   
    }   
    cursor.close();  
}

table表中添加数据

ContentValues values = new ContentValues();  
values.put("column1", "text");  
values.put("column2", 1);  
getContentResolver().insert(uri, values);

将待添加的数据组装到 ContentValues 中,然后调用 ContentResolver 的insert()方法,将 Uri 和 ContentValues 作为参数传入即可。

现在如果我们想要更新这条新添加的数据,把 column1 的值清空,可以借助 ContentResolver 的 update()方法实现:

ContentValues values = new ContentValues();  
values.put("column1", "");  
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new  String[] {"text", "1"}); 

注意上述代码使用了 selection 和 selectionArgs 参数来对想要更新的数据进行约束,以 防止所有的行都会受影响。

可以调用 ContentResolver 的 delete()方法将这条数据删除掉:

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

2.读取系统联系人

先自己在手机/模拟器中手动添加几个联系人

新建一个 ContactsTest 项目

添加一个ListView

ps:我用的依然是ConstraintLayout布局而不是LinearLayout布局

<ListView
    android:id="@+id/contacts_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:ignore="MissingConstraints"
    />

修改MainActivity

添加两个参数

package com.example.contactstest;

import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        ListView contactsView = findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
        contactsView.setAdapter(adapter);
        if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.
                READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{ android.Manifest.
                    permission.READ_CONTACTS }, 1);
        } else {
            readContacts();
        }
    }

    @SuppressLint("Range")
    private void readContacts() {
        Cursor cursor = null;
        try {
            // 查询联系人数据
            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 number = cursor.getString(cursor.getColumnIndex
                            (ContactsContract.CommonDataKinds.Phone.NUMBER));
                    contactsList.add(displayName + "\n" + number);
                }
                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(this, "You denied the permission", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

在 onCreate()方法中,首先获取了 ListView 控件,并给它设置了适配器,然后开始调用运行时权限的处理逻辑

在用户授权之后调用 readContacts()方法来读取系统联系人信息

重点看一下 readContacts()方法

使用了 ContentResolver 的 query() 方法来查询系统的联系人数据。不过传入的 Uri 参数是ContactsContract.CommonDataKinds.Phone 类已经做好的封装,提供的一个 CONTENT_URI 常量,这个常量就是使用 Uri.parse()方法解析出来的结果。

然后对 Cursor 对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号这一列对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER。

两个数据都取出之后,将它们拼接,并在中间加上换行符,然后将拼接后的数据添加到 ListView 的数据源里,并刷新一下 ListView。最后将 Cursor 对象关闭掉

然后再AndroidManifest.xml里做一下权限声明:

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

然后运行

4.创建自己的内容提供器

如果想要实现跨程序共享数据的功能,可以通过新建一个类去继承 ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,我们在使用子类继承 它的时候,需要将这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;   
    }   
    @Override   
    public Uri insert(Uri uri, ContentValues values) {   
        return null;   
    }   
    @Override   
    public int update(Uri uri, ContentValues values, String selection, String[]   selectionArgs) {   
        return 0;   
    }   
    @Override   
    public int delete(Uri uri, String selection, String[] selectionArgs) {   
        return 0;   
    }   
    @Override   
    public String getType(Uri uri) {   
        return null;   
    }  
}
  1. onCreate() 初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回 true 表示内容提供器初始化成功,返回 false 则表示失败。

  2. query() 从内容提供器中查询数据。使用 uri 参数来确定查询哪张表,projection 参数用于确定查询哪些列,selection 和 selectionArgs 参数用于约束查询哪些行,sortOrder 参数用于对结果进行排序,查询的结果存放在 Cursor 对象中返回。

  3. insert() 向内容提供器中添加一条数据。使用 uri 参数来确定要添加到的表,待添加的数据保存在 values 参数中。添加完成后,返回一个用于表示这条新记录的 URI。

  4. update() 更新内容提供器中已有的数据。使用 uri 参数来确定更新哪一张表中的数据,新数据保存在 values 参数中,selection 和 selectionArgs 参数用于约束更新哪些行,受影响的行数将作为返回值返回。

  5. delete() 从内容提供器中删除数据。使用 uri 参数来确定删除哪一张表中的数据,selection 和 selectionArgs 参数用于约束删除哪些行,被删除的行数将作为返回值返回。

  6. getType() 根据传入的内容 URI 来返回相应的 MIME 类型。 可以看到,几乎每一个方法都会带有 Uri 这个参数,这个参数也正是调用 ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的 Uri 参数进行解析,从中分析出调用方期望访问的表和数据。

一个标准的内容 URI 写法是这样的:

content://com.example.app.provider/table1

这就表示调用方期望访问的是 com.example.app 这个应用的 table1 表中的数据。

我们还可以在这个内容 URI 的后面加上一个 id,如下所示: content://com.example.app.provider/table1/1

这就表示调用方期望访问的是 com.example.app 这个应用的 table1 表中 id 为 1 的数据。

以路径结尾就表示期望访问该表中所有的数据,以 id 结尾就表示期望访问该表中拥有相应 id 的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容 URI,规则如下:

*表示匹配任意长度的任意字符。

#表示匹配任意长度的数字。

所以,一个能够匹配任意表的内容 URI 格式就可以写成:

content://com.example.app.provider/*

而一个能够匹配 table1 表中任意一行数据的内容 URI 格式就可以写成:

content://com.example.app.provider/table1/#

我们再借助 UriMatcher 这个类就可以轻松地实现匹配内容 URI 的功能。

UriMatcher 中提供了一个 addURI()方法,这个方法接收 3 个参数,可以分别把 authority、path 和一个自 定义代码传进去。这样,当调用 UriMatcher 的 match()方法时,就可以将一个 Uri 对象传入, 返回值是某个能够匹配这个 Uri 对象所对应的自定义代码,利用这个代码,我们就可以判断出调 用方期望访问的是哪张表中的数据了。修改 MyProvider 中的代码:

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;
private static UriMatcher uriMatcher;
static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
    uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);
    uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_DIR);
    uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);
}



@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
    switch (uriMatcher.match(uri)) {
        case TABLE1_DIR:
            // 查询 table1 表中的所有数据
            break;
        case TABLE1_ITEM:
            // 查询 table1 表中的单条数据
            break;
        case TABLE2_DIR:
            // 查询 table2 表中的所有数据
            break;
        case TABLE2_ITEM:
            // 查询 table2 表中的单条数据
            break;
        default:
            break;
    }
    return null;
}

MyProvider 中新增了 4 个整型常量

其中 TABLE1_DIR 表示访问 table1 表中的所有数据,TABLE1_ITEM 表示访问 table1 表中的单条数据,TABLE2_DIR 表示访问 table2 表中的所有数据,TABLE2_ITEM 表示访问 table2 表中的单条数据。

接着在静态代码块里我们创建了 UriMatcher 的实例,并调用 addURI()方法,将期望匹配的内容 URI 格式传递进去,这里传入的路径参数是可以使用通配符的。然后当 query()方法被调用的时候,就会通过 UriMatcher 的 match()方法对传入的 Uri 对象进行匹配,如果发现 UriMatcher 中某个内容 URI 格式成功匹配了该 Uri 对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了。

insert()、update()、delete()这 几个方法的实现也是差不多的,它们都会携带 Uri 这个参数,然后同样利用 UriMatcher 的 match() 方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。

关于getType()方法。它是所有的内容提供器都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型。一个内容 URI 所对应的 MIME 字符串主要由 3 部分组成,Android 对这 3 个部分做了如下格式规定。

  1. 必须以 vnd 开头。

  2. 如果内容 URI 以路径结尾,则后接 android.cursor.dir/,如果内容 URI 以 id 结尾, 则后接 android.cursor.item/。

  3. 最后接上 vnd..

所以,对于 content://com.example.app.provider/table1 这个内容 URI,它所对应的 MIME 类型就可以写成:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1

对于 content://com.example.app.provider/table1/1 这个内容 URI,它所对应的 MIME 类型就可以写成:

vnd.android.cursor.item/vnd.com.example.app.provider.table1

来实现 getType()方法中的逻辑:

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

到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用 ContentResolver 来访问我们程序中的数据。

5.实现跨程序数据共享

我们使用上一章中 DatabaseTest 项目的基础上继续开发,通过内容提供器来给它加入外部访问接口。

首先将 MyDatabaseHelper 中使用 Toast 弹出创建 数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用 Toast。

然后创建一个内容提供 器,右击 com.example.databasetest 包→New→Other→Content Provider

我们将内容提供器命名为 DatabaseProvider,authority 指定为 com.example.databasetest.provider

接着我们修改 DatabaseProvider 中的代码

package com.example.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;

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.example.databasetest.provider";
    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;
    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);
    }

    public DatabaseProvider() {
    }

    @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;
            default:
                break;
        }
        return deletedRows;
    }

    @Override
    public String getType(Uri uri) {
        // TODO: Implement this to handle requests for the MIME type of the data
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.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;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        // TODO: Implement this to initialize your content provider on startup.
        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;
            default:
                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;
            default:
                break;
        }
        return updatedRows;
    }
}

代码虽然很长,不过这些内容都非常容易理解。

首先在类的一开始定义了 4 个常量,分别用于表示访问 Book 表中的所有数据、访问 Book 表中的单条数据、访问 Category 表中的所有数据和访问 Category 表中的单条数据。

然后在静态代码块里对 UriMatcher 进行了初始化操作,将期望匹配的几种 URI 格式 添加了进去。

接下来就是每个抽象方法的具体实现了。

onCreate()方法,这个方法的代码很短, 就是创建了一个 MyDatabaseHelper 的实例,然后返回 true 表示内容提供器初始化成功,这时数据库就已经完成了创建或升级操作

query()方法,在这个方法中先获取到了 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要访问哪张表,再调用 SQLiteDatabase 的 query()进行查询,并将 Cursor 对象返回就好了。注意当访问单条数据的时候有一个细节,这里调用了 Uri 对象的 getPathSegments()方法,它会将内容 URI 权限之后的部分以“/”符号进行分割,并把分割后 的结果放入到一个字符串列表中,那这个列表的第 0 个位置存放的就是路径,第 1 个位置存放的就是 id 。得到了 id 之后,再通过 selection 和 selectionArgs 参数进行约束,就实现了查询单条数据的功能。

insert()方法,同样它也是先获取到了 SQLiteDatabase 的实例,然后根据传入 的 Uri 参数判断出用户想要往哪张表里添加数据,再调用 SQLiteDatabase 的 insert()方法进行添加就可以了。注意 insert()方法要求返回一个能够表示这条新增数据的 URI,所以我们还需要调用 Uri.parse()方法来将一个内容 URI 解析成 Uri 对象,当然这个内容 URI 是以新增数据的 id 结尾的。

update()方法,也是先获取 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要更新哪张表里的数据,再调用 SQLiteDatabase 的 update()方法进行更新就好了,受影响的行数将作为返回值返回。

delete()方法,这里仍然是先获取到 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要删除哪张表里的数据,再调用 SQLiteDatabase 的 delete()方法进行删除就好了,被删除的行数将作为返回值返回。

getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的。

内容提供器一定要在 AndroidManifest.xml 文件中注册才可以使用。 不过,我们是使用 Android Studio 的快捷方式创建的内容提供器,注册这一步已经被自动完成了。

打开 AndroidManifest.xml

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

标签内出现了一个新的标签,我们使用它来对 DatabaseProvider 这个内容提供器进行注册

我们需要将 DatabaseTest 程序从模拟器中删除掉,以防止上一章中产生的遗留数据对我们造成干扰。然后运行一下项目,将 DatabaseTest 程序重新安装。

创建一个新项目 ProviderTest,我们通过这个程序去访问 DatabaseTest 中的数据。

创建4个按钮用来完成增删改查

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="添加"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="查询"
    app:layout_constraintEnd_toEndOf="@+id/button"
    app:layout_constraintStart_toStartOf="@+id/button"
    app:layout_constraintTop_toBottomOf="@+id/button" />

<Button
    android:id="@+id/button3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="修改"
    app:layout_constraintEnd_toEndOf="@+id/button2"
    app:layout_constraintStart_toStartOf="@+id/button2"
    app:layout_constraintTop_toBottomOf="@+id/button2" />

<Button
    android:id="@+id/button4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="删除"
    app:layout_constraintEnd_toEndOf="@+id/button3"
    app:layout_constraintStart_toStartOf="@+id/button3"
    app:layout_constraintTop_toBottomOf="@+id/button3" />

然后修改 MainActivity

package com.example.providertest;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    private String newId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 添加数据
                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", 22.85);
                Uri newUri = getContentResolver().insert(uri, values);
                newId = newUri.getPathSegments().get(1);
            }
        });
        Button button1 = findViewById(R.id.button2);
        button1.setOnClickListener(new View.OnClickListener() {
            @SuppressLint("Range")
            @Override
            public void onClick(View view) {
                // 查询数据
                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 button2 = findViewById(R.id.button3);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 更新数据
                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 button3 = findViewById(R.id.button4);
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 删除数据
                Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
                getContentResolver().delete(uri, null, null);
            }
        });
    }
}

添加数据的时候, 调用了 Uri.parse()方法将内容 URI 解析成 Uri 对象,然后把要添加的数据都存放到 ContentValues 对象中,接着调用 ContentResolver 的 insert()方法执行添加操作。 注意 insert()方法会返回一个 Uri 对象,这个对象中包含了新增数据的 id,我们通过 getPathSegments()方法将这个 id 取出,稍后会用到它。

查询数据的时候,同样是调用了 Uri.parse()方法将一个内容 URI 解析成 Uri 对象,然后调用 ContentResolver 的 query()方法去查询数据,查询的结果当然还是存放在 Cursor 对象 中的。之后对 Cursor 进行遍历,从中取出查询结果,并一一打印出来。

更新数据的时候,也是先将内容 URI 解析成 Uri 对象,然后把想要更新的数据存放到 ContentValues 对象中,再调用 ContentResolver 的 update()方法执行更新操作。 注意这里我们为了不想让 Book 表中的其他行受到影响,在调用 Uri.parse()方法时,给内容 URI 的尾部增加了一个 id,而这个 id 正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据,Book 表中的其他行都不会受影响。

删除数据的时候,也是使用同样的方法解析了一个以 id 结尾的内容 URI,然后调用 ContentResolver 的 delete()方法执行删除操作。由于我们在内容 URI 里指定了一个 id,因此只会删掉拥有相应 id 的那行数据,Book 表中的其他数据都不会受影响。

ps:如果你和我一样使用的是Android11及以上的设备或模拟器那么你还需要在AndroidManifest文件中添加一个标签

<queries>
        <package android:name="com.example.databasetest"/>
<!--    或者<provider android:authorities="com.example.databasetest.provider" />-->
    </queries>
    
    
    <application
    ...

这是因为Android11时修改了软件之间的交互,我们需要在queries标签内声明要访问的目标app

以及记得将之前的创建数据库的版本改为1,否则会提示不能将数据区的版本从4降级为2

然后运行安装

运行后会提示找不到com.example.databasetest.provider

这时返回桌面,将DatabaseTese打开并挂在后台

再返回ProviderTest进行操作就可以了

(如果杀掉后台再进行操作还是会报错找不到)

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值