第一行代码笔记⑥
6.1 内容提供器
内容提供器主要是在不同的应用程序之间实现数据共享的功能。不同于前面讲的文件存储和SharedPreferences存储中的两种全局读写操作,内容提供器可以选择只对哪一部分数据进行共享,保障隐私数据不会有泄露的风险。
6.2 运行时权限
权限可以分为普通权限和危险权限
参考安卓系统完整的权限列表
在程序运行时申请权限
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable 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 view) {
if (ContextCompat.checkSelfPermission(MainActivity.this, android.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) {
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 permisson", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序是不可以擅自做主去支撑这些危险操作的。
1、首先判断用户是否授权,借助ContextCompat.checkSelfPermission()方法,第一个参数是Context,第二个参数是具体的权限名,然后我们用方法的返回值与PackageManager.PERMISSION_GRANTED作比较,相等则表示用户已经授权。
2、如果已经授权就直接调用call()方法就行了,如果没有授权,就需要调用ActivityCompat.requestPermissions()方法去获取权限,该方法调用以后,会弹出一个权限申请框,用户可以选择同意或者拒绝。无论是哪种结果,最终都会回调到onRequestPermissionsResult()方法中,而授权的结果则会封装在grantResults参数中。
6.3 内容提供器的用法
访问其他程序中的数据
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器,给我们程序的数据提供外部访问接口。
1 ContentResolver的基本用法
通过Context中的getContentResolver()获取实例,ContentResolver类提供了一系列方法可以对数据进行CRUD操作。但是不同于SQLiteDatabase,它不用接收表名参数,而是使用一个Uri参数代替,这个参数被称为内容URI,由authority和path组成,前者是用于对不同的应用程序做区分,一般为了避免冲突,都会采用程序包名的方式进行命名。path则是用来区分不同的表。标准的内容URI格式如下
content://com.example.app.provider/table1
")
获取Uri对象
Uri uri = Uri.parse("content://com.example.app.provider/table1
接着使用Uri对象去查询表中数据
2 读取系统联系人
新建ContactsTest项目
首先新建activity_main.xml文件,只包含一个ListView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
再写MainActivity的代码
package com.example.contactstest;
import android.Manifest;
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.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
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(@Nullable 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, android.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, @NonNull String[] permissions, @NonNull 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:
}
}
}
这里我们事先在模拟器中添加了两个用户。
在 oncreate()方法中,我们首先获取了ListView控件的实例,并给它设置好了适配器,然后开始调用运行时权限的处理逻辑,因为READ_CONTACTS权限是属于危险权限的。关于运行时权限的处理流程相信你已经熟练掌握了,这里我们在用户授权之后调用readContacts()方法来读取系统联系人信息。
下面重点看一下readContacts()方法,可以看到,这里使用了ContentResolver的 query()方法来查询系统的联系人数据。不过传入的 uri 参数怎么有些奇怪啊?为什么没有调用Uri.parse()方法去解析一个内容URI字符串呢?这是因为ContactsContract.CommonData-Kinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量,而这个常量就是使用Uri.parse()方法解析出来的结果。接着我们对Cursor对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds .Phone. DISPLAY_NAME,联系人手机号这一列对应的常量是 ContactsContract.CommonData-Kinds.Phone.NUMBER。两个数据都取出之后,将它们进行拼接,并且在中间加上换行符,然后将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListView。最后千万不要忘记将Cursor对象关闭掉。
最后设置权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
模拟结果
3 创建自己的内容提供器
前面已经提到过,如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器。ContentProvider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。新建MyProvider继承自ContentProvider。
onCreate()
初始化内容提供器的时候调用,通常会在这里完成对数据库的创建和升级等操作。
返回true表示内容提供器初始化成功,返回false则表示失败。
只有当存在ContentResolver尝试访问程序中的数据的时候,内容提供器才会被初始化。
query()
从内容提供器中查询数据
uri参数去确定查询哪张表
projection参数用于查询哪些列
selection和selectionArgs参数用于约束查询哪些行
sortOrder参数用于对结果进行排序
查询的结果存放在Cursor对象中返回
insert()
向内容提供器中添加一条数据
使用uri参数来确定要添加的表
待添加的数据保存在values参数中
添加完成后,返回一个用于表示这条新纪录的URI
update()
更新内容提供器中已有的数据
使用uri参数来确定更新哪张表
新数据保存在values参数中
selection和selectionArgs用于约束更新哪些行
受影响的行数将作为返回值返回
delete()
从内容提供器中删除数据
使用uri参数来确定删除哪一张表中的数据
selection和selectionArgs用于约束删除哪些行
被删除的行数将作为返回值返回
getType()
根据传入的内容URI来返回相应的MIME类型
标准的内容URI写法是
content://com.example.app.provider/table1 以路径结尾表示期望访问该表中的所有数据
vnd.android.cursor.dir/vnd.com.example.dpp.provider.table
content://com.example.app.provider/table1/1 以id结尾表示期望访问该表中拥有相应id的数据
vnd.android.cursor.item/vnd.com.example.dpp.provider.table
可以使用通配符去匹配
content://com.example.app.provider/* 匹配任意表内容
content://com.example.app.provider/table1/# 匹配table1表中任意一行数据
getType()方法是所有内容提供器都需要提供的一个方法,用于获取Uri对象所对应的MIME类型,一个内容URI所对应的MIME字符串由三部分组成
必须以vnd开头
如果以路径结尾,则后接android.cursor.dir/ 如果以Id结尾,则后接android.cursor.item/
最后接上vnd.<authority>.<path>
4 实现跨数据共享
新建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;
import java.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) {
// Implement this to handle requests to delete one or more rows.
// 删除数据
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
// at the given 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";
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
// 添加数据
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() {
// TODO: Implement this to initialize your content provider on startup.
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO: Implement this to handle query requests from clients.
// 查询数据
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) {
// TODO: Implement this to handle requests to update one or more rows.
// 更新数据
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;
}
}
首先定义四个常量,用于匹配UriMatcher所期望的URI
onCreate方法创建一个MyDatabaseHelper实例,返回true表示内容提供器初始化成功
通过uri.getPathSegments().get(1)得到Id
前面的方法是以为"/"进行分割,第0个位置是路径,第1个位置是id