《第一行代码》Android学习笔记(7)内容提供器 —— ContentProvider

目录

1. 运行时权限

1.1 权限机制

2.2 运行时申请权限

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

2.1 ContentResolver 基本用法

2. URI:统一资源标识符 Uniform Resource Identifier

2.2 读取系统联系人

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

# 保证隐私数据不会泄露:UriMatcher

3.1 跨程序数据共享:provider 作为外部访问接口


内容提供器:用于不同的应用程序之间实现数据共享功能。

 

1. 运行时权限

 

1.1 权限机制

普通权限:不会直接威胁到用户安全和隐私的权限,系统自动授权

危险权限:可能触及隐私或影响安全性的权限,必须手动点击授权才行。如地理位置、联系人等

 

Android 中所有危险权限,一共9组24个

 

权限组名

权限名称

CALENDAR(日历)

READ_CALENDAR

WRITE_CALENDAR

CAMERA(相机)

CAMERA

CONTACTS(联系人)

READ_CONTACTS

WRITE_CONTACTS

GET_ACCOUNTS

LOCATION(位置)

ACCESS_FINE_LOCATION

ACCESS_COARSE_LOCATION

MICROPHONE(麦克风)

RECORD_AUDIO

PHONE(手机)

READ_PHONE_STATE

CALL_PHONE

ERAD_CALL_LOG

WRITE_CALL_LOG

ADD_VOICEMAIL

USE_SIP

PROCESS_OUTGOING_CALLS

SENSORS(传感器)

BODY_SENSORS

SMS(短信)

SEND_SMS

RECEIVE_SMS

READ_SMS

RECEIVE_WAP_PUSH

RECEIVE_MMS

STORAGE(存储卡)

READ_EXTERNAL_STORAGE

WRITE_EXTERNAL_STORAGE

 

完整权限列表:https://developer.android.google.cn/reference/android/Manifest.permission.html

 

2.2 运行时申请权限

申请 CALL_PHONE 权限

1. 按钮点击事件构建一个隐式 Intent,操作放到 try/catch 中防止程序崩溃

public void onClick(View v) {
    try {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel:10086"));
        startActivity(intent);
    } catch (SecurityException e) {
        e.printStackTrace();
    }
}
// 清单文件加上权限
<uses-permission android:name="android.permission.CALL_PHONE" />

低于 Android 6.0 系统的手机正常运行,更高版本系统上,提示权限禁止。因为 6.0及以上系统使用危险权限都必须有运行时权限处理。

 

2. 用户授权

① 判断用户是否已授权,ContextCompat.checkSelfPermission,方法的返回值和 PackageManager.PERMISSION_GRANTED 作比较

② 如果已授权直接 call,否则调用 ActivityCompat.requestPermissions 向用户申请授权

③ 弹出权限申请对话框,将结果回调到 onRequestPermissionsResult

 

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button makeCall = findViewById(R.id.make_call);
    makeCall.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (ContextCompat.checkSelfPermission(MainActivity.this,
                    Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{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, "You denied the permission",
                        Toast.LENGTH_SHORT).show();
            }
            break;
        default:
            break;
    }
}

 

关闭权限:Setting -> Apps -> RuntimePermissionTest -> Permissions

 

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

内容提供器用法有两种:

① 使用现有的 ContentProvider 读取和操作相应程序中的数据

② 创建自己的 ContentProvider 给程序的数据提供外部访问接口,允许其他程序访问

 

2.1 ContentResolver 基本用法

1. getContentResolver():获取 ContentResolver 类的实例

insert, update, delete, query

 

2. URI:统一资源标识符 Uniform Resource Identifier

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

头部是协议声明

authority:用于区分不同应用程序,以包名方式命名

path:区分同一程序中不同的表

id:访问表中 id 为1 的数据

content://com.example.contactstest.provider/table1/1

 

通配符:

  • *:匹配任意长度的任意字符
  • #:匹配任意长度的数字
// 匹配任意表内容
content://com.example.contactstest.provider/*
// 匹配table1 表中任意一行数据的内容
content://com.example.contactstest.provider/table1/#

3. 将 URI 字符串解析成 Uri 对象,然后作为参数传入

Uri uri = Uri.parse("content://com.example.contactstest.provider/table1");

4. 增删改查

query:返回 Cursor 对象

Cursor cursor = getContentResolver().query(
    uri,//表路径
    projection,//查询的列名
    selection, //where 约束条件
    selectionArgs, //占位符提供具体的值
    sortOrder); // 排序方式
    
// 移动游标位置遍历 cursor 所有行
if (cursor != null) {
    while (cursor.moveToNext()) {
        String column1= cursor.getString(cursor.getColumnIndex("column1"));
    }
    cursor.close();
}

 

insert

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

 

update

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

 

delete

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

 

 

2.2 读取系统联系人

 

 

public class MainActivity extends AppCompatActivity {
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        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, Manifest.permission.READ_CONTACTS)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{
                    Manifest.permission.READ_CONTACTS}, 1);
        } else {
            readContacts();
        }
    }

    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:
        }
    }
}

//Manifest 声明权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>

Notes:

① ListView 显示联系人信息

② 危险权限,需要用户授权

③ getContentResolver 查询联系人数据,清单文件加上权限声明

 

 

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

1. 新建类继承 ContentProvider

public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

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

    /**
     * 根据传入的 URI 返回相应 MIME 类型
     * @param uri
     * @return
     */
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

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

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

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

 

2. 使用 UriMatcher,匹配内容 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;
private static UriMatcher uriMatcher;

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

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

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    switch (uriMatcher.match(uri)) {
        case TABLE1_DIR:
            // 查询 table1 表中所有数据
            break;
        case TABLE1_ITEM:
            // 查询 table1 表中单条数据
            break;
        case TABLE2_DIR:
            // // 查询 table2 表中所有数据
            break;
... 

Notes:

① addURI 将 URI 内容跟常量匹配,供后面使用

② query 匹配传入的 Uri 对象,判断调用方期待访问的是什么数据

 

getType:返回 MIME类型

格式规定:

  • 必须以 vnd 开头;
  • 如果以路径结尾,则接 android.cursor.dir/ ,如果以 id 结尾,则接 android.cursor.item/;
  • 最后接上 vnd.<authority>.<path>
public String getType(@NonNull Uri uri) {
    switch (uriMatcher.match(uri)) {
        case TABLE1_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.providertest.provider.table1";
        case TABLE1_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.providertest.provider.table1";
        case TABLE2_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.providertest.provider.table2";
        case TABLE2_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.providertest.provider.table2";
        default:
            break;
    }
    return null;
}

 

# 保证隐私数据不会泄露:UriMatcher

所有增删改查操作都一定要匹配相应的内容 URI 格式才能进行。

 

3.1 跨程序数据共享:provider 作为外部访问接口

1. 打开DatabaseTest,建 DatabaseProvider 作为外部访问接口,供 ProviderTest 应用访问

new -> Other -> ContentProvider

 

2. 修改 DatabaseProvider 代码

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() {
    }

    /**
     * 创建 MyDatabaseHelper 实例,此时数据库完成创建或升级
     * @return
     */
    @Override
    public boolean onCreate() {
        Log.d(TAG, "onCreate: ");
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

    /**
     * 根据uri 判断用户想要访问哪张表,然后调用 SQLiteDatabase 的 query 查询
     * @param uri
     * @param projection
     * @param selection
     * @param selectionArgs
     * @param sortOrder
     * @return
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        Log.d(TAG, "query: ");
        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;
            // getPathSegments 会将 uri 权限之后的部分以 "/" 分割,并把结果存到字符串列表中
            // 列表第 0 位置存放路径,1 位置存放 id。通过此方法取出 id
            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;
    }

    /**
     * 调用数据库 insert 方法添加数据
     * @param uri
     * @param values
     * @return 返回能够表示新增数据的 URI,拼凑内容 URI 以 id 结尾,然后解析成 Uri对象
     */
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Log.d(TAG, "insert: ");
        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("Book", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    /**
     *
     * @param uri
     * @param values
     * @param selection
     * @param selectionArgs
     * @return 返回受影响的行数
     */
    @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;
    }

    @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) {
        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";
            default:
                break;
        }
        return null;
    }
}

 

3. 在 AndroidManifest 文件注册内容提供器,并允许外部访问

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

4. 编写 ProviderTest,用来访问 DatabaseTest 的数据库

public static final String AUTHORITY = "com.example.databasetest.provider";
...
switch (v.getId()) {
    case R.id.add_data:
        Log.d(TAG, "onClick: add_data");
        uri = Uri.parse("content://" + AUTHORITY + "/book");
        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);//保留新增数据的 id,过后使用
        Log.d(TAG, "onClick: newId = " + newId);
        break;
    case R.id.query_data:
        Log.d(TAG, "onClick: query_data");
        uri = Uri.parse("content://" + AUTHORITY + "/book");
        Cursor cursor = getContentResolver().query(uri, null,
                null, null, null);
        if (cursor != null) {
            StringBuilder builder = new StringBuilder();
            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"));
                builder.append("book name is " + name + "\n");
                builder.append("book author is " + author + "\n");
                builder.append("book pages is " + pages + "\n");
                builder.append("book price is " + price + "\n\n");
            }
            queryResult.setText(builder.toString());
            cursor.close();
        }
        break;
    case R.id.update_data:
        uri = Uri.parse("content://" + AUTHORITY + "/book/" + newId);
        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);
        break;
    case R.id.delete_data:
        uri = Uri.parse("content://" + AUTHORITY + "/book/" + newId);
        getContentResolver().delete(uri, null, null);
        break;
    default:
        break;
}

比如查询,先执行按钮下的查询操作 getContentResolver().query 这一句,跳转到 DatabaseTest 项目中的 DatabaseProvider 类中,执行 query() 方法,在里面进行数据库查询然后返回 cursor。

 

 

演示:首先在 DatabaseTest 创建数据库,然后退出,到 ProviderTest 中对数据库执行增删改查操作。

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值