什么是内容提供器?
在安卓(Android)系统中,内容提供器(Content Provider)是一个提供数据共享机制的组件,允许一个应用程序共享其数据给其他应用程序。内容提供器是应用程序间数据共享的标准接口,它为数据存取提供了一套统一的API。
内容提供器的主要特点和功能有:
-
数据抽象:内容提供器为存储在不同位置(如SQLite数据库、文件、网络等)的数据提供了一个统一的接口。其他应用程序不需要知道数据的具体存储方式和位置,只需通过内容提供器即可访问数据。
-
数据安全:内容提供器提供了数据访问的权限控制,可以限制哪些应用或组件可以访问数据,以及它们可以执行的操作(如读、写、修改等)。
-
URI寻址:内容提供器使用统一资源标识符(URI)来表示数据。每个内容提供器都有一个唯一的URI,这样其他应用程序可以通过URI找到并访问数据。
-
数据共享:内容提供器是安卓中实现跨应用程序数据共享的主要机制。例如,安卓系统的联系人、日历、短信等数据都是通过内容提供器提供给其他应用程序的。
-
数据修改通知:内容提供器可以使用观察者模式,当数据发生变化时通知其他应用程序或组件。
使用内容提供器的典型场景包括:
- 当你想要在应用程序之间共享数据时。
- 当你需要为应用程序提供一个统一的数据访问接口时。
- 当你需要控制数据访问的权限时。
总的来说,内容提供器是安卓中为应用程序提供数据访问和数据共享功能的关键组件,它使应用程序能够安全、高效地共享和管理数据。
为什么需要内容提供器?
在学习内容提供器之前我们需要学习以下安卓运行时权限的知识,
如何运行时申请权限?
静态申请很简单,只需要在Manifest文件中,写出权限即可,运行时申请比较重要:
在安卓6.0之后推出了对于某些危险权限,需要用户同意才可以。
分为两种:
1.“饿汉式”:
就是在程序启动的时候进行权限申请,也就是在最开始的活动进行
在这里,我们使用拨号权限,进行示例:
//代码
package com.example.activitytest;
import static android.Manifest.permission.CALL_PHONE;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.example.permissionUtil.PermissionUtil;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn;
String[] permission = {CALL_PHONE};
public static final int CALL_PHONE_CODE = 1;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
btn = findViewById(R.id.button_1);
btn.setOnClickListener(this);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1) {
if (PermissionUtil.checkResult(grantResults)) {
Toast.makeText(this, "已经同意", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "已经拒绝", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.button_1) {
//如果点击了按钮那么,
if (PermissionUtil.check(this, permission, CALL_PHONE_CODE)) {
} else {
Toast.makeText(this, "进行权限申请", Toast.LENGTH_SHORT).show();
}
}
}
}
package com.example.permissionUtil;
import android.app.Activity;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
public class PermissionUtil {
//三个参数:1.跳转回调方法的时候吗,需要知道跳转到哪一个活动,需要回调方法所在活动
//2.权限的数组
//3.请求码,回调方法可以重复使用,可以通过请求码确定你申请了哪一个权限
public static boolean check(Activity activity, String[] permissions, int requestCode) {
//在安卓6.0之后
boolean flag = false;
for (String permission : permissions) {
int check = PackageManager.PERMISSION_GRANTED;
if (check != ContextCompat.checkSelfPermission(activity,permission)) {
ActivityCompat.requestPermissions(activity, permissions, requestCode);
flag = true;
break;
}
}
if (flag) {
return false;
}
return true;
}
public static boolean checkResult(int[] ints) {
for (int i : ints) {
if (i != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
此处我们使用了一种更加普遍的方式,用一个String数组将所有需要申请的权限装了起来(可多用)。
此处我新建了一个工具类,封装了用于处理权限信息的函数。
简单来说,这就是一个由用户授权我们去执行一些操作。第一步,先检查所要申请的权限是否已经被授权了,有了函数checkPermission(),参数有三个:第一个,被冲写的回调方法onRequsetPermissionResult()所在的活动,第二个,权限数组,里面是所要申请的所有权限。
第三个是请求码,因为要请求的权限可能会很多,所有需要做一个区分,比如联系人的读取和写入,短信的读取和写入,直接可以分为两个数组来写。
checkPermission函数主体是:通过循环将每一个权限使用ContextCompat类中的checkSelfPermission方法检查是否已经被授权,如果没有则需要进行申请,使用活动中自带的方法requestPermission,然后将执行结果反馈在那个回调方法onRequestPermissionResult,其中grantResult数组中的值就是对应权限数组中每个权限的申请结果。
类名中存在Compat字段的类:
"Compat" 是 "Compatibility"(兼容性)的缩写,表示在软件开发中保持向后兼容性的目标。当一个软件或库具有向后兼容性,意味着它可以在新版本中保持与旧版本相同的接口和行为,以便现有的代码和功能可以继续正常工作,而不需要进行大规模的修改。
在 Android 开发中,你可能会经常看到 "compat" 这个词,它通常出现在库、类名或方法名中,指示这些库或方法旨在保持向后兼容性。例如,Android Support Library(现在已被 AndroidX 取代)中的许多类和方法都带有 "Compat" 后缀,以表示它们提供了与不同版本的 Android 平台兼容的功能。
举例来说,如果你想要使用某个功能在所有 Android 版本上保持一致的行为,你可能会使用对应的 "Compat" 类或方法,而不是直接使用平台提供的原生方法。这样可以确保你的应用在不同的 Android 版本上都能够正常运行。
总之,"compat" 表示兼容性,用于标识在软件开发中为了保持向后兼容性而采取的措施和库。
2.“饱汉式”:
在使用权限的时候,进行申请。
这两种申请区分很简单,饱汉式需要触发条件,饿汉式直接在onCreate中执行就可以了
提供器怎么使用?
内容提供器分为两个部分:
1.从系统程序中获得数据:
直接使用系统程序已经写好的内容提供者,使用getContentResolver();方法解析数据,然后进行调用。
我们app相当于是一个客户端,向系统应用申请数据,这里将读取联系人信息,作为例子:
我们使用uri获取我们要读取的表的资源:
与SQLite不同的是,ContentResolver中的增删改查方法都是不接受表明参数的,而是使用一个uri参数代替,这个参数被称为内容uri,内容uri给表提供了唯一表示符,它主要由两部分组成:authority和path,authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式进行应用,然后把authority和path结合,内容Uri就会变成,类似于:com.example.mainactivity/table_1,目前还很辨认出这两个字符串就是uri,所以我们要在字符串的头部加上协议声明,因此uri最标准的写法应该是:
content//:com.example.mainactivity/table_1
内容uri,可以很清楚的表达出我们想要访问哪个程序中哪张表里的内容,所以ContentResolver中的增删改查方法才都接受uri作为对象参数,得到uri的字符串之后,我们需要将其解析为对象参数。
Uri uri = Urti.parse("content//:com.example.mainactivity/table_1");
接下来我们从系统电话本中获取联系人信息:
package com.example.test;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import com.example.permissionUtil.PermissionUtil;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
public List<String> list = new ArrayList<>();
public static final int REQUEST_CODE = 1;
private ContentResolver contentResolver;
private ArrayAdapter<String> mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//使用listView将联系人信息放在这里面
ListView lv = findViewById(R.id.lv_PhoneContacts);
mAdapter = new ArrayAdapter<>(this, R.layout.item_contacts, list);
lv.setAdapter(mAdapter);
//先申请权限
if (PermissionUtil.checkPermission(this, new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE)) {
Toast.makeText(this, "用户已经同意", Toast.LENGTH_SHORT).show();
}
Log.d("wang", "执行成功");
contentResolver = getContentResolver();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE) {
if (PermissionUtil.checkGrant(grantResults)) {
Toast.makeText(this, "用户已同意", Toast.LENGTH_SHORT).show();
//执行数据读取
readData();
}
}
}
private void readData() {
Cursor cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
//注解是表明不会
@SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
@SuppressLint("Range") String phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
list.add(name + "\n" + phoneNumber);
}
mAdapter.notifyDataSetChanged();
}
}
}
工具类:
package com.example.permissionUtil;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionUtil {
public static boolean checkPermission(Activity activity, String[] permissions, int requestCode) {
for (String permission :
permissions) {
int check = PackageManager.PERMISSION_GRANTED;
if (check != ContextCompat.checkSelfPermission(activity, permission)) {
ActivityCompat.requestPermissions(activity, permissions, requestCode);
return false;
}
}
return true;
}
public static boolean checkResults(int[] ints) {
for (int i :
ints) {
if (i != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
// public static boolean checkPermission(Activity act, String[] permissions, int requestCode) {
// //如果是是大于安卓6.0
//
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// //默认已经授权了
// int check = PackageManager.PERMISSION_DENIED;
// for (String permission : permissions) {
// //默认是同意的
// //一个一个进行检查
// check = ContextCompat.checkSelfPermission(act, permission);
// //如果没有授权
// if (check != PackageManager.PERMISSION_GRANTED) {
// break;
// }
// }
// if (check != PackageManager.PERMISSION_GRANTED) {
// //如果未开启权限,则请求系统弹窗,好让用户选择
// //requestCode是让我们确定是在请求哪一个的权限
// ActivityCompat.requestPermissions(act, permissions, requestCode);
// return false;
// }
// }
// return true;
// }
//
// public static boolean checkGrant(int[] grantResults) {
// if (grantResults != null) {
// for (int i : grantResults) {
// if (i != PackageManager.PERMISSION_GRANTED) {
// return false;
// }
// }
// return true;
// }
// return false;
// }
//}
在onCreate方法中我们请求权限,
ContactsContract.CommonDataKinds.Phone类已经给我们将uri封装好了
因此我们直接在contentResolver中直接使用查询方法。返回值是一个cursor,遍历结果集的每一行将联系人的姓名和手机号依次取出,放在listView的条目里面。然后进行刷新就可以了,
记得在Manifest里面声明读取系统联系人的姓名,
<user-permission android:name="adnroid.permission.READ_CONTACTS"
声明权限是向Android系统注册应用程序需要使用的权限的一种方式,这样系统才会在必要时提醒用户授权。如果不声明权限,应用程序可能会出现无法正常运行或者被系统直接拒绝访问相关资源的情况。
2.从其他app中获得数据:
自己创建一个服务端ContentProvider,在另一个软件中客户端使用getContentResolver();方法进行解析。
我们将数据的提供者一般称为服务端,索取者称为客户端
先简单说一下我们要使用的服务端的架构:
我们客户端访问服务端的SQLite数据库,MyDBH继承于SQLiteOpenHelper作为对外部开放的接口,在服务端创建一个内容提供者,操作MyDBH类,实现对数据库的操作,然后在客户端使用ContentResolver进行解析读取数据,实现跨进程数据的通信。
构建服务端的代码:
创建一个数据库,使用SQLiteOpenHelper对数据库进行操作,并不用实现增删改查的操作,
package server.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class MyDBH extends SQLiteOpenHelper {
public static final String DB_NAME = "user.db";
public static final int TABLE_VERSION = 1;
public static final String TABLE_NAME = "TABLE_NAME";
private static MyDBH mHelper = null;
public MyDBH(Context c) {
super(c, DB_NAME, null, TABLE_VERSION);
}
public static final String NAME = "name";
public static final String EMAIL = "email";
@Override
public void onCreate(SQLiteDatabase db) {
//创建了数据库
String sql = "CREATE TABLE IF NOT EXISTS TABLE_NAME (" + NAME + " VARCHAR NOT NULL, " + EMAIL + " VARCHAR NOT NULL);";
db.execSQL(sql);
}
public static MyDBH getInstance(Context c) {
if (mHelper == null) {
mHelper = new MyDBH(c);
}
return mHelper;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
创建一个类继承内容提供者,ContentProvider,实现下面这些方法
利用MyDBH实现对数据库的操作,使用
MyDBH.getInstance(getContext()) //获取数据库实例
myDBH.getWritableDatabase(); //建立写连接
先和数据库建立连接,然后直接使用获取的实例进行操作:删改查
package server.provider;
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;
import android.util.Log;
import server.database.MyDBH;
public class UserInfoProvider extends ContentProvider {
private MyDBH myDBH;
private static final int USERS = 1;
private static final int USER = 2;
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static {
//往Uri匹配器中添加指定的数据路径
//USERS和USER就是请求码
URI_MATCHER.addURI(UserInfoContent.AUTHORITIES, "/TABLE_NAME", USERS);
URI_MATCHER.addURI(UserInfoContent.AUTHORITIES, "/TABLE_NAME/#", USER);
}
@Override
public boolean onCreate() {
Log.d("wang", "onCreate");
//在最开始的时候进行实例的创建
myDBH = MyDBH.getInstance(getContext());
//如果provider被加载成功,就会返回true
return true;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 可写入的数据库
Log.d("wang", "insert");
// 添加调试输出
Log.d("wang", "Inserting data into table: " + myDBH.TABLE_NAME);
Log.d("wang", "Data to insert: " + values.toString());
// 连接数据库
SQLiteDatabase sb = myDBH.getWritableDatabase();
sb.insert(myDBH.TABLE_NAME, null, values);
//如果数据发生变化,直接向内容监视器进行报告,并将uri更新到发生变化的行号
/*if (rowId > 0) { // 判断插入是否执行成功
// 如果添加成功,就利用新记录的行号生成新的地址
Uri newUri = ContentUris.withAppendedId(UserInfoContent.CONTENT_URI, rowId);
// 通知监听器,数据已经改变
getContext().getContentResolver().notifyChange(newUri, null);
}*/
return uri;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.d("wang", "query");
SQLiteDatabase db = myDBH.getReadableDatabase();
Cursor q = db.query(myDBH.TABLE_NAME, projection, selection, selectionArgs, null, null, null);
return q;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
switch (URI_MATCHER.match(uri)) {
case USERS:
SQLiteDatabase db1 = myDBH.getWritableDatabase();
count = db1.delete(myDBH.TABLE_NAME, selection, selectionArgs);
db1.close();
break;
// case USER:
// //这个,是通过传入有id 的uri进行判断,删除哪些数据
// //由于我并没有在苦衷定义id所以,就不许需要这个了
// String id = uri.getLastPathSegment();//获得最后的id是多少
// SQLiteDatabase db2 = myDBH.getWritableDatabase();
// count = db2.delete(myDBH.TABLE_NAME, selection, selectionArgs);
// db2.close();
// break;
}
return count;
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
}
(选看)
服务端还有一个需要注意的点,由于客户端ContentResolver每个方法都要使用uri,我们要对传入服务端的uri进行解析,所以就有了通配符的使用,便于用户查询。使用uri匹配器,更加准确的定位到对应表,使用switch方法根据请求码的不同,在同一个删除,添加或者查询方法中执行不同操作。
一个可以匹配任意表内的内容Uri格式可以写为:
content//:com.example.包名/*
一个可以匹配表内任意一行的内容Uri格式可以写为:
content//:com.example.包名/表名/#
这样服务端的代码就完成了?
除此之外,我们还需要在Manifest文件里面设置一些东西,最好将authorities设置为自己服务端按这个软件所在的包名,便于区分
接下来就到了客户端,客户端比较简单,只需要用好ContentResolver就好了:
package com.example.client;
import android.net.Uri;
public class UserInfoContent {
public static final String AUTHORITIES = "server.provider.UserInfoProvider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIES + "/TABLE_NAME");
public static final String USER_NAME = "name";
public static final String USER_EMAIL = "email";
}
package com.example.client;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
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 android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.example.user.User;
public class ContentWriteActivity extends AppCompatActivity implements View.OnClickListener {
private EditText et_name;
private EditText et_email;
private TextView tv_query;
private Button btn_del;
//唯一的问题就是数据库的关闭和打开都不是时机,可以即将数据库的关闭操作放在onStop方法里面
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_write);
et_email = findViewById(R.id.et_email);
et_name = findViewById(R.id.et_name);
tv_query = findViewById(R.id.tv_query);
btn_del = findViewById(R.id.btn_del);
btn_del.setOnClickListener(this);
//两个按钮
Button btn_save = findViewById(R.id.btn_save);
btn_save.setOnClickListener(this);
Button btn_query = findViewById(R.id.btn_query);
btn_query.setOnClickListener(this);
}
public void onClick(View v) {
if (v.getId() == R.id.btn_save) {
// 保存按钮点击事件
// try {
ContentValues values = new ContentValues();
values.put(UserInfoContent.USER_NAME, et_name.getText().toString());
values.put(UserInfoContent.USER_EMAIL, et_email.getText().toString());
Uri insertUri = getContentResolver().insert(UserInfoContent.CONTENT_URI, values);
if (insertUri != null) {
Toast.makeText(this, "成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "失败", Toast.LENGTH_SHORT).show();
}
} else if (v.getId() == R.id.btn_query) {
// 查询按钮点击事件
Cursor cursor = getContentResolver().query(UserInfoContent.CONTENT_URI, null, null, null, null);
if (cursor != null) {
StringBuilder data = new StringBuilder();
while (cursor.moveToNext()) {
// 将所有行的数据连接到 'data' StringBuilder
@SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex(UserInfoContent.USER_NAME));
@SuppressLint("Range") String email = cursor.getString(cursor.getColumnIndex(UserInfoContent.USER_EMAIL));
data.append("姓名: ").append(name).append(", 电子邮件: ").append(email).append("\n");
}
tv_query.setText(data.toString());
cursor.close();
Toast.makeText(this, "查询成功", Toast.LENGTH_SHORT).show();
}
} else if (v.getId() == R.id.btn_del) {
String name = et_name.getText().toString();
String email = et_email.getText().toString();
int count = getContentResolver().delete(UserInfoContent.CONTENT_URI, "name=?", new String[]{name});
if (count > 0) {
Toast.makeText(this, "删除成功", Toast.LENGTH_SHORT).show();
}
}
}
}
在客户端的Manifest文件中加上
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
表示可以访问所有软件包,也可以选择访问固定包名,此处为了方便就直接使用了所有包名。