四大组件:ContentProvider学习

权限以及ContentProvider学习

1.运行时权限

在这里插入图片描述

权限参照表,如果是表中的权限,需要进行运行时权限处理,如果不在这张表只需要在AndroidManifest中添加一下声明权限就可以了

1.1在程序运行时申请权限

新建一个项目,在这个项目的基础上来学习运行时权限的使用方法,以CALL_PHONE权限作为示例

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/make_call"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="打电话" />
</LinearLayout>

布局定义一个按钮,在点击按钮时就会触发拨打电话的逻辑,接着修改MainActivity中的代码

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        Button makeCall = findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try{
                    Intent intent = new Intent(Intent.ACTION_CALL);
                    intent.setData(Uri.parse("tel:10086"));
                    startActivity(intent);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
    }
}

可以看到,在按钮的点击事件中,构建了一个隐士Intent,Intent的action指定为Intent.CALL_PHONE,这是一个系统内置的打电话动作,在data部分制定了协议是tel,号码是10086。

接下来修改AndroidMainfest中声明如下权限

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

开始调试并且点击按钮发现什么都没有发生,

在这里插入图片描述

看到错误提示发现是Permission Denial,可以看出是由于去哪线被禁止所导致的,因为6.0以上的系统在实用为先权限是都必须要进行运行时权限处理。

修改主活动的代码

package com.example.runtimepermissiontest;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
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;

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, 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, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, 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:
        }
    }
}
  1. 权限判断:程序运行中执行危险操作(如拨打电话 )前,用 ContextCompat.checkSelfPermission() 方法判断用户是否已授权。该方法接收 Context 和具体权限名(如 Manifest.permission.CALL_PHONE ),返回值与 PackageManager.PERMISSION_GRANTED 比较,相等则已授权,不等则未授权。
  2. 已授权处理:若已授权,直接执行对应危险操作逻辑,文中将拨打电话逻辑封装在 call() 方法。
  3. 未授权处理:未授权时,调用 ActivityCompat.requestPermissions() 向用户申请授权。该方法需传入 Activity 实例、含权限名的 String 数组、唯一请求码(文中为 1 ),调用后系统弹出权限申请对话框。
  4. 授权结果回调:用户处理权限申请(同意或拒绝 )后,系统回调 onRequestPermissionsResult() 方法,授权结果封装在 grantResults 参数。需判断结果,同意则调用 call() 执行操作,拒绝则放弃操作并弹出失败提示(如文中提示 “You denied the permission” )。
  5. 实际运行示例:重新运行程序点击 Make Call 按钮,因未授权首次会弹出权限申请对话框,若用户点击 DENY 拒绝,会执行对应拒绝后的提示逻辑 。

发现系统会弹窗申请授权,授权成功就可以正常拨打电话了

2.内容提供器

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。那么接下来我们就一个一个开始学习吧,首先从使用现有的内容提供器开始。

如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。Android 系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。下面我们就来看一看,内容提供器到底是如何使用的。

2.1ContentResolver的基本用法

内容提供器数据访问关键类与操作

  • 访问工具:应用程序访问内容提供器共享数据,需借助 ContentResolver 类,可通过 ContextgetContentResolver() 方法获取其实例 。
  • CRUD 操作方法ContentResolver 提供 insert()(添加数据 )、update()(更新数据 )、delete()(删除数据 )、query()(查询数据 )方法,和 SQLiteDatabase 的 CRUD 方法功能对应,但参数有区别 。

内容 URI 相关

  • 作用与组成ContentResolver 增删改查方法不用表名参数,而用 Uri 参数(即内容 URI ),为内容提供器数据建立唯一标识符,由 authority(区分不同应用,常以程序包名命名,如 com.example.app.provider )和 path(区分同一应用不同表,如 /table1/table2 )两部分组成 。
  • 标准格式:内容 URI 需在组合 authoritypath 后,头部加协议声明 content:// ,如 content://com.example.app.provider/table1 ,能清晰表达要访问哪个程序哪张表的数据 ,这也是 ContentResolver 方法接收 Uri 而非表名参数的原因(表名无法体现所属应用 )。

URI可以非常清楚的表达想要访问那个程序的那张表的数据,因此ContentResolver的CURD都是用Uri对象来作为参数,如果使用表名的话系统将无法得知我们期望访问的是那个应用程序里面的表

得到内容URI字符串,需要将他解析成Uri对象才可以作为参数传入,解析方法:

调用Uri.parse(),就可将URI字符串解析Uri对象了

一、查询数据(query 方法 )
  1. 方法调用:通过

    getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder)
    

    查询内容提供器中数据,参数与 SQLite的query类似但更简洁,各参数对应 SQL 语句部分及作用:

    query 方法参数对应 SQL 部分描述
    urifrom table_name指定查询某应用程序下某张表
    projectionselect column1, column2指定查询列名
    selectionwhere column = value指定 where 约束条件
    selectionArgs-where 占位符提供具体值
    sortOrderorder by column1, column2指定查询结果排序方式
  2. 结果处理:返回 Cursor 对象,通过移动游标遍历数据,示例代码:

if (cursor != null) {
    while (cursor.moveToNext()) {
        String column1 = cursor.getString(cursor.getColumnIndex("column1"));
        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
    }
    cursor.close();
}
二、添加数据(insert 方法 )
  1. 操作步骤:创建 ContentValues 组装数据,调用 getContentResolver().insert(uri, values) 插入,示例:
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
三、更新数据(update 方法 )
  1. 操作步骤:创建 ContentValues 设更新值,调用 getContentResolver().update(uri, values, selection, selectionArgs) ,用 selectionselectionArgs 约束更新范围,避免全表更新,示例:
ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"text", "1"});

这些操作围绕内容提供器的 CRUD(创建、读取、更新、删除 ),利用 ContentResolver 结合 UriContentValues 等完成跨程序数据交互 。

2.2读取系统联系人

新建一个ContactsTest项目,希望读取出来的系统联系人可以在list中显示出来

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <ListView
        android:id="@+id/contacts_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

MainActivity

package com.example.contactstest;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.ContactsContract;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

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);
        setContentView(R.layout.activity_main);
        ListView contactsView = (ListView) findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter<>(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()) {
                    // 获取联系人姓名
                    @SuppressLint("Range") String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                    // 获取联系人手机号
                    @SuppressLint("Range") 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, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, 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:
        }
    }
}

主要看一下readContact方法,可以看到使用了ContentResolver的query方法来查询系统用的联系人数。传入的uri没有用.parse()去解析原因是ContactsContract.CommonDataKinds.Phone类已经帮我做好了封装提供了一个CONTENT_URI常量,这个常量就是.parse解析的,接着对Cursor对性进行遍历,将信息放到一个list之中。拼接并且在中间夹上换行符。最后通知ListView更新,别忘了关闭cursor。

,最后声明一下权限

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

这样就可以显示联系人了

小结:我们学习了如何在自己的程序中访问其他程序的数据。总的来说就是要获取到程序的内容URI,借助ContentResolver进行CURD。

2.3创建自己的内容提供器

  • 内容提供器创建方式:实现跨程序共享数据,官方推荐用内容提供器,可通过新建类继承 ContentProvider 来创建自定义内容提供器。
  • 抽象方法重写要求ContentProvider 类有 6 个抽象方法,子类继承时需全部重写,这 6 个方法分别是 onCreate()query()insert()update()delete()getType() ,用于实现数据的创建、查询、插入、更新、删除以及获取数据类型等操作逻辑。
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;
    }
}
一、ContentProvider 核心方法解析
  1. onCreate()
    • 调用时机:初始化内容提供器时触发,仅当有 ContentResolver 尝试访问程序数据时,内容提供器才会初始化。
    • 功能:常在此完成数据库创建、升级等操作,返回 true 表示初始化成功,false 表示失败。
  2. query()
    • 功能:从内容提供器查询数据。
    • 参数作用:
      • uri:确定查询哪张表;
      • projection:确定查询哪些列;
      • selection + selectionArgs:约束查询哪些行(类似 SQL 的 WHERE 条件 );
      • sortOrder:对查询结果排序。
    • 返回:查询结果封装在 Cursor 对象中返回。
  3. insert()
    • 功能:向内容提供器添加一条数据。
    • 参数作用:uri 确定添加到的表,values 保存待添加数据。
    • 返回:添加后,返回表示新记录的 URI。
  4. update()
    • 功能:更新内容提供器中已有数据。
    • 参数作用:uri 确定更新的表,values 存新数据,selection + selectionArgs 约束更新哪些行。
    • 返回:受影响的行数作为返回值。
  5. delete()
    • 功能:从内容提供器删除数据。
    • 参数作用:uri 确定删除的表,selection + selectionArgs 约束删除哪些行。
    • 返回:被删除的行数作为返回值。
  6. getType()
    • 功能:根据传入的内容 URI,返回对应的 MIME 类型。
二、内容 URI 解析与格式
  1. 核心作用ContentProvider 方法多数带 Uri 参数,用于标识调用方期望访问的表和数据,需解析 Uri 以明确操作目标。
  2. 标准格式
    • 访问表中所有数据:content://com.example.app.provider/table1 (路径结尾,对应 com.example.app 应用的 table1 表全量数据 )。
    • 访问表中指定 ID 数据:content://com.example.app.provider/table1/1 (ID 结尾,对应 com.example.app 应用 table1 表中 id=1 的数据 )。
  3. 通配符匹配
    • 匹配 table1 表任意一行数据的内容 URI 格式:content://com.example.app.provider/table1/# ,借助这些通配符可灵活匹配不同需求的内容 URI,用于在内容提供器等场景中识别、处理数据访问请求 。
    • 匹配任意表内容的URI: content://com.example.app.provider/*

这些要点是理解 ContentProvider 工作机制、实现跨程序数据共享的基础,开发自定义内容提供器时,需围绕方法逻辑和 URI 解析做具体功能实现。

接着使用UriMatcher就饿可以匹配URI了。

  1. UriMatcher 作用与使用流程
    • 作用:用于轻松实现内容 URI 的匹配功能,帮助判断调用方期望访问的是哪张表的数据。
    • 使用流程:
      • 定义整型常量,分别标识不同的访问场景(如访问某表所有数据、某表单条数据 )。
      • 在静态代码块中创建 UriMatcher 实例,通过 addURI() 方法传入 authoritypath(可含通配符 )和自定义代码,将期望匹配的内容 URI 格式注册到 UriMatcher 中。
      • query()ContentProvider 方法中,调用 UriMatchermatch() 方法传入 Uri 对象,根据返回的自定义代码判断访问场景,进而执行对应的数据操作逻辑。
  2. 常量与匹配规则
    • 定义 4 类整型常量(以 table1table2 为例 ):TABLE1_DIR(访问 table1 所有数据 )、TABLE1_ITEM(访问 table1 单条数据 )、TABLE2_DIR(访问 table2 所有数据 )、TABLE2_ITEM(访问 table2 单条数据 )。
    • addURI() 方法中路径参数可使用通配符,如 table2/# 匹配 table2 表中带 id 的单条数据访问场景 。
public class MyProvider extends ContentProvider {

    // 定义整型常量,标识不同访问场景
    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;

    // 静态代码块初始化 UriMatcher
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 注册期望匹配的内容 URI 格式及对应自定义代码
        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);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // 通过 UriMatcher 匹配 Uri,根据返回代码判断访问场景
        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;
    }

    // ContentProvider 其他抽象方法(onCreate、insert、update、delete、getType)需按实际逻辑重写,此处省略...
    @Override
    public boolean onCreate() {
        return false;
    }

    @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. getType() 方法作用与 MIME 类型规则
    • getType() 是内容提供器必须实现的方法,用于获取 Uri 对应的 MIME 类型。
    • MIME 字符串组成规则:
      • 必须以 vnd 开头。
      • 内容 URI 以路径结尾,后接 android.cursor.dir/;以 id 结尾,后接 android.cursor.item/
      • 最后接 vnd..
    • 示例:
      • content://com.example.app.provider/table1(路径结尾 )→ vnd.android.cursor.dir/vnd.com.example.app.provider.table1
      • content://com.example.app.provider/table1/1id 结尾 )→ vnd.android.cursor.item/vnd.com.example.app.provider.table1
  2. MyProvidergetType() 实现:通过 UriMatcher 匹配 Uri,根据不同匹配结果(对应访问 table1/table2 的所有数据或单条数据场景 ),返回符合规则的 MIME 类型字符串。
  3. 数据安全保障:内容提供器的 CRUD 操作需匹配内容 URI 格式,未向 UriMatcher 添加隐私数据 URI,外部程序无法访问,保障数据安全。
public class MyProvider extends ContentProvider {

    // 其他代码(如 UriMatcher 定义、query 等方法 )省略...

    @Override
    public String getType(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 null;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值