【Android】ContentProvider基本概念

ContentProvider

Android权限机制详解

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
            package="com.example.broadcasttest"> 
            <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
            ... 
    </manifest>

这是之前所学的东西,应用这个防止系统阻止我们监听开机广播

其实用户主要在两个方面得到了保护。一方面,如果用户在低于Android 6.0系统的设备上安装该程序,会在安装界面给出提醒。这样用户就可以清楚地知晓该程序一共申请了哪些权限,从而决定是否要安装这个程序。

另一方面,用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况,这样该程序申请的所有权限就尽收眼底,什么都瞒不过用户的眼睛,以此保证应用程序不会出现各种滥用权限的情况。

在Android 6.0系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,也应该可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。

当然,并不是所有权限都需要在运行时申请,对于用户来说,不停地授权也很烦琐。Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限。其实还有一些特殊权限,不过这些权限使用得相对较少

普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作,比如在BroadcastTest项目中申请的权限就是普通权限。

危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能。

危险权限:

在这里插入图片描述

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

运行时申请权限

1.检查App是否开启了指定权限权限检查需要调用ContextCompat的checkSelfPermission方法,该方法的第一个参数为活动实例,第二个参数为待检查的权限名称,例如存储卡的写权限名为Manifest.permission.WRITE_EXTERNAL_STORAGE。注意checkSelfPermission方法的返回值,当它为PackageManager.PERMISSION_GRANTED时表示已经授权,否则就是未获授权。

2.请求系统弹窗,以便用户选择是否开启权限一旦发现某个权限尚未开启,就得弹窗提示用户手工开启,这个弹窗不是开发者自己写的提醒对话框,而是系统专门用于权限申请的对话框。调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口,该方法的第一个参数为活动实例,第二个参数为待申请的权限名称数组,第三个参数为本次操作的请求代码。

3.判断用户的权限选择结果然而上面第二步的requestPermissions方法没有返回值,那怎么判断用户到底选了开启权限还是拒绝权限呢?其实活动页面提供了权限选择的回调方法onRequestPermissionsResult,如果当前页面请求弹出权限申请窗口,那么该页面的J的Java代码必须重写onRequestPermissionsResult方法,并在该方法内部处理用户的权限选择结果。

具体到编码实现上,前两步的权限校验和请求弹窗可以合并到一块,先调用checkSelfPermission方法检查某个权限是否已经开启,如果没有开启再调用requestPermissions方法请求系统弹窗。合并之后的检查方法代码示例如下,此处代码支持一次检查一个权限,也支持一次检查多个权限

1、使用ContextCompat.checkSelfPerSelfPermission()检查权限状态

2、获取的权限状态和PackageManager.PERMISSION_GRANTEN比较

3、如果没有权限则向用户申请权限ActivityCompat.requesPermissions()

4、重写onRequestPermissionsResult

  • PackageManager.PERMISSION_GRANTED:表示已授予权限。0
  • PackageManager.PERMISSION_DENIED:表示权限被拒绝。-1
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;

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

        binding = ActivityMainBinding.inflate(getLayoutInflater());

        setContentView(binding.getRoot());

        binding.button1.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[]{Manifest.permission.CALL_PHONE}, 1);
                } else {
                    call();
                }
            }
        });
    }

    protected 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) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 1:
                //grantResults储存了用户的授权结果
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call();
                }else {
                    Toast.makeText(this, "No Permission", Toast.LENGTH_SHORT).show();
                }
        }
    }
}

ContextCompat.checkSelfPermission()是一个用于检查权限状态的方法,它是Android支持库(Support Library)中的一个实用工具类。该方法用于在应用中检查特定权限是否已经被授予。该方法获取两个参数,第一个是context,第二个是具体的权限名。

ActivityCompat.requestPermissions()它会在运行时向用户请求权限。具有三个三个参数。

  1. 第一个是context
  2. 第二个是一个权限数组(保存需要获取的权限名)
  3. 第三个参数是请求码,用于标识此特定权限请求(后续在onRequestPermissionsResult()方法中处理权限请求结果时,系统会提供这个请求码,帮助你识别是哪个权限请求的结果。)。

onRequestPermissionsResult该重写方法具有三个参数:

  1. int requestCode: 是请求权限时传递的请求码,用于标识特定的权限请求。
  2. String[] permissions: 是请求的权限数组,包含所请求的权限的名称。
  3. int[] grantResults: 是对应权限数组的授权结果数组,它包含用户对权限请求的响应,如果授权,则值为PackageManager.PERMISSION_GRANTED,否则为PackageManager.PERMISSION_DENIED。

通过ContentProvider封装数据

Android号称提供了4大组件,分别是活动Activity、广播Broadcast、服务Service和内容提供器ContentProvider。其中内容提供器涵盖与内部数据存取有关的一系列组件,完整的内容组件由内容提供器ContentProvider、内容解析器ContentResolver、内容观察器ContentObserver三部分组成。

ContentProvider给App存取内部数据提供了统一的外部接口,让不同的应用之间得以互相共享数据。

像上一章提到的SQLite可操作应用自身的内部数据库;

上传和下载功能可操作后端服务器的文件;

而ContentProvider可操作当前设备其他应用的内部数据,它是一种中间层次的数据存储形式。

在实际编码中,ContentProvider只是服务端App存取数据的抽象类,开发者需要在其基础上实现一个完整的内容提供器,并重写下列数据库管理方法。

  • onCreate:创建数据库并获得数据库连接。
  • insert:插入数据。
  • delete:删除数据。
  • update:更新数据。
  • query:查询数据,并返回结果集的游标。
  • getType:获取内容提供器支持的数据类型。

如果计划共享数据,则可使用 ContentProvider。如果不打算共享数据,也可使用 ContentProvider,因为它们可以提供很好的抽象。此抽象可让修改应用数据存储实现,同时不会影响依赖数据访问的组件(或者其他现有应用)。在此情况下,受影响的只有 ContentProvider 的实现,而非访问的组件(或者其他现有应用)。

ContentProvider 的优点,主要包括以下两点:

  • 1、内容提供程序可精细控制数据访问权限。可以选择仅在应用内限制对内容ContentProvider 的访问,授予访问其他应用数据的权限,或配置读取和写入数据的不同权限。
  • 2、可以使用内容提供程序将细节抽象化,以用于访问应用中的不同数据源。例如,某个应用可能会在 SQLite 数据库中存储结构化记录,以及视频和音频文件。

ContentProvider 基础知识

在这里插入图片描述

在这里插入图片描述

需要查询 ContentProvider 程序中的数据,可以使用 ContentResolver 的query(Uri,projection,selection,selectionArgs,sortOrder)方法:

uri:映射至提供程序中名为 table_name 的表。

内容 URI 用来在 ContentProvider 程序中标识数据。内容 URI 包括整个ContentProvider 程序的符号名称(其授权)和指向表的名称(路径)。

当调用客户端访问ContentProvider 程序中的表时,该表的内容 URI 将是其参数之一。ContentProvider 使用内容 URI的路径部分选择需访问的表。通常 ,ContentProvider 程序会为其公开的每个表显示一条路径。

如下:projection:是检索到的每个行所应包含的列的数组。

selection:指定选择行的条件。

selectionArgs:没有完全等效项,选择参数会替换选择子句中的 ? 占位符。

sortOrder:指定在返回的 Cursor 中各行的显示顺序。

权限

提供方定义权限,调用方声明权限。如通讯录 ContentProvider 程序定义的"android.permission.READ_CONTATACTS 权限 ,

我 们 在 读 取 通 讯 录 联 系 人 信 息 的 时 候 需 要 加 上 : <uses-permissionandroid:name="android.permission.READ_CONTATACTS"/>权限。

协定类的使用

协定类可定义一些常量,帮助应用使用内容 URI、列名称、Intent 操作以及内容提供程序的其他功能。提供程序不会自动包含协定类,因此提供程序的开发者需定义这些类,并将其提供给其他开发者。Android 平台中的许多提供程序在android.provider 软件包中均拥有对应的协定类。

MIME 类型引用

内容提供程序可以返回标准 MIME 媒体类型和/或自定义 MIME 类型字符串。

MIME 类型采用以下格式:type/subtype。

标准类型:text/html;img/png …

自定义:

如果 URI 指向单行:android.cursor.item/

如果 URI 指向多行:android.cursor.dir/

Provider-specific 部分:vnd..,name 和 type 需要我们定义的内容。

最后我们需要掌握的系统 contentprovider,如联系人、短信、通话记录、日历、图库等

使用

ContentProvider作为中间接口,本身并不直接保存数据,而是通过SQLiteOpenHelper与SQLiteDatabase间接操作底层的数据库。所以要想使用ContentProvider,首先得实现SQLite的数据库帮助器,然后由ContentProvider封装对外的接口。以封装用户信息为例,具体步骤主要分成以下3步。

1.编写用户信息表的数据库帮助器这个数据库帮助器就是常规的SQLite操作代码,

2.编写内容提供器的基础字段类该类需要实现接口BaseColumns,同时加入几个常量定义。详细代码示例如下:

public class UserInfoContent implements BaseColumns {
    // 这里的名称必须与AndroidManifest.xml里的android:authorities保持一致
    public static final String AUTHORITIES = "com.example.chapter07.provider.UserInfoProvider";
    // 内容提供器的外部表名
    public static final String TABLE_NAME = UserDBHelper.TABLE_NAME;
    // 访问内容提供器的URI
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIES + "/user");
    // 下面是该表的各个字段名称
    public static final String USER_NAME = "name";
    public static final String USER_AGE = "age";
    public static final String USER_HEIGHT = "height";
    public static final String USER_WEIGHT = "weight";
}

3.通过右键菜单创建内容提供器

在创建对话框的Class Name一栏填写内容提供器的名称,比如UserInfoProvider;

在URI Authorities一栏填写URI的授权串,比如“com.example.chapter07.provider.UserInfoProvider”;

然后单击对话框右下角的Finish按钮,完成提供器的创建操作。

上述创建过程会自动修改App模块的两处地方,一处是往AndroidManifest.xml添加内容提供器的注册配置,配置信息示例如下

<!-- provider的authorities属性值需要与Java代码的AUTHORITIES保持一致 --><providerandroid:name=".provider.UserInfoProvider"android:authorities="com.example.chapter07.provider.UserInfoProvider"android:enabled="true"android:exported="true" />

另一处是在包名目录下生成名为UserInfoProvider.java的代码文件,打开一看发现该类继承了ContentProvider,并且提示重写onCreate、insert、delete、query、update、getType等方法,以便对数据进行增删改查等操作。这个提供器代码显然只有一个框架,还需补充详细的实现代码,为此重写onCreate方法,在此获取用户信息表的数据库帮助器实例,其他insert、delete、query等方法也要加入对应的数据库操作代码,

public class UserInfoProvider extends ContentProvider {
    private final static String TAG = "UserInfoProvider";
    private UserDBHelper userDB; // 声明一个用户数据库的帮助器对象

    public static final int USER_INFO = 1; // Uri匹配时的代号
    public static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        // 往Uri匹配器中添加指定的数据路径
        uriMatcher.addURI(UserInfoContent.AUTHORITIES, "/user", USER_INFO);
    }

    // 创建ContentProvider时调用,可在此获取具体的数据库帮助器实例
    @Override
    public boolean onCreate() {
        userDB = UserDBHelper.getInstance(getContext(), 1);
        return true;
    }

    // 插入数据
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (uriMatcher.match(uri) == USER_INFO) {
            // 匹配到了用户信息表
            // 获取SQLite数据库的写连接
            SQLiteDatabase db = userDB.getWritableDatabase();
            // 向指定的表插入数据,返回记录的行号
            long rowId = db.insert(UserInfoContent.TABLE_NAME, null, values);
            if (rowId > 0) {
                // 判断插入是否执行成功
                // 如果添加成功,就利用新记录的行号生成新的地址
                Uri newUri = ContentUris.withAppendedId(UserInfoContent.CONTENT_URI, rowId);
                // 通知监听器,数据已经改变
                getContext().getContentResolver().notifyChange(newUri, null);
            }
            db.close(); // 关闭SQLite数据库连接
        }
        return uri;
    }

    // 根据指定条件删除数据
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;
        if (uriMatcher.match(uri) == USER_INFO) {
            // 匹配到了用户信息表
            // 获取SQLite数据库的写连接
            SQLiteDatabase db = userDB.getWritableDatabase();
            // 执行SQLite的删除操作,并返回删除记录的数目
            count = db.delete(UserInfoContent.TABLE_NAME, selection, selectionArgs);
            db.close(); // 关闭SQLite数据库连接
        }
        return count;
    }

    // 根据指定条件查询数据库
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Cursor cursor = null;
        if (uriMatcher.match(uri) == USER_INFO) {
            // 匹配到了用户信息表
            // 获取SQLite数据库的读连接
            SQLiteDatabase db = userDB.getReadableDatabase();
            // 执行SQLite的查询操作
            cursor = db.query(UserInfoContent.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
            // 设置内容解析器的监听
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
        }
        return cursor; // 返回查询结果集的游标
    }

    // 获取Uri支持的数据类型,暂未实现
    @Override
    public String getType(Uri uri) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    // 更新数据,暂未实现
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

通过ContentResolver访问数据

在这里插入图片描述

上一小节提到了利用ContentProvider封装服务端App的数据,如果客户端App想访问对方的内部数据,就要借助内容解析器ContentResolver。内容解析器是客户端App操作服务端数据的工具,与之对应的内容提供器则是服务端的数据接口。在活动代码中调用getContentResolver方法,即可获取内容解析器的实例。ContentResolver提供的方法与ContentProvider一一对应,比如insert、delete、query、update、getType等,甚至连方法的参数类型都雷同。以添加操作为例,针对前面UserInfoProvider提供的数据接口,下面由内容解析器调用insert方法,使之往内容提供器插入一条用户信息,记录添加代码如下所示:

private void addUser(UserInfo user) {
    ContentValues values = new ContentValues();
    values.put("name", user.getName());
    values.put("age", user.getAge());
    values.put("height", user.getHeight());
    values.put("weight", user.getWeight());
    values.put("married", 0);
    values.put("update_time", DateUtil.getNowDateTime(""));

    // 通过内容解析器往指定Uri添加用户信息
    getContentResolver().insert(UserInfoContent.CONTENT_URI, values);
}

删除:

getContentResolver().delete(UserInfoContent.CONTENT_URI, "1=1", null);

查询操作

private void showAllUser() {
    List<UserInfo> userList = new ArrayList<UserInfo>();
    
    // 通过内容解析器从指定Uri中获取用户记录的游标
    Cursor cursor = getContentResolver().query(UserInfoContent.CONTENT_URI, null, null, null, null);
    
    // 循环取出游标指向的每条用户记录
    while (cursor.moveToNext()) {
        UserInfo user = new UserInfo();
        user.setName(cursor.getString(cursor.getColumnIndex(UserInfoContent.USER_NAME)));
        user.setAge(cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_AGE)));
        user.setHeight(cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_HEIGHT)));
        user.setWeight(cursor.getFloat(cursor.getColumnIndex(UserInfoContent.USER_WEIGHT)));
        
        userList.add(user); // 添加到用户信息列表
    }
    
    cursor.close(); // 关闭数据库游标
    
    // 显示用户总数
    String contactCount = String.format("当前共找到%d个用户", userList.size());
    tv_desc.setText(contactCount);
    
    ll_list.removeAllViews(); // 移除线性布局下面的所有下级视图
    for (UserInfo user : userList) { // 遍历用户信息列表
        String contactDesc = String.format(
            "姓名为%s,年龄为%d,身高为%d,体重为%f",
            user.getName(), user.getAge(), user.getHeight(), user.getWeight()
        );
        
        TextView tv_contact = new TextView(this); // 创建一个文本视图
        tv_contact.setText(contactDesc);
        tv_contact.setTextColor(Color.BLACK);
        tv_contact.setTextSize(17);
        
        // 设置文本视图的内部间距
        int pad = Utils.dip2px(this, 5);
        tv_contact.setPadding(pad, pad, pad, pad);
        
        ll_list.addView(tv_contact); // 把文本视图添加至线性布局
    }
}
      
        TextView tv_contact = new TextView(this); // 创建一个文本视图
        tv_contact.setText(contactDesc);
        tv_contact.setTextColor(Color.BLACK);
        tv_contact.setTextSize(17);
        
        // 设置文本视图的内部间距
        int pad = Utils.dip2px(this, 5);
        tv_contact.setPadding(pad, pad, pad, pad);
        
        ll_list.addView(tv_contact); // 把文本视图添加至线性布局
    }
}
  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值