本篇文章主要介绍以下几个知识点:
- Android 运行时权限;
- 内容提供器
7.1 运行时权限
在介绍内容提供器之前先来了解了解 Android 运行时权限。
7.1.1 Android 权限机制
Android 把所有的权限归成两类:普通权限和危险权限。
普通权限
不会直接威胁到用户的安全和隐私的权限,对于这部分的权限申请,系统会自动帮我们进行授权。危险权限
可能触及用户隐私,或对设备安全性造成影响的权限,如获取联系人信息、地理位置等,对于这部分的权限申请,必须由用户手动点击授权才可以,否则程序无法使用相应的功能。
下面列出了 Android 中所有的危险权限:
上表中每个危险权限都属于一个权限组,若用户同意授权某个权限名,那么该权限所对应的权限组中的其他权限也会同时跟着被授权。
注:访问 http://developer.android.com/reference/android/Manifest.permission.html 可以查看完整的权限列表。
7.1.2 在运行程序时申请权限
接下来通过个例子来介绍如何在运行程序时申请权限。
此次的例子是通过点击界面上的一个按钮来直接拨打电话。拨打电话时需要声明上节表中的 CALL_PHONE 这个危险权限。
首先,在 AndroidManifest.xml 文件中声明权限:
然后在布局 activity_call_phone.xml 中添加个按钮:
接着修改 activity 中代码如下:
public class CallPhoneActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_call_phone);
Button btn_call_phone = (Button) findViewById(R.id.btn_call_phone);
btn_call_phone.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
// Intent.ACTION_CALL 是系统内置的打电话动作(而Intent.ACTION_DIAL指打开拨号界面,不需要声明权限)
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
});
}
}
在 Android 6.0 前,拨打电话功能的实现很简单,上面代码就把拨打电话功能实现了,在低于 Android 6.0 系统的手机上可以正常运行,但在 Android 6.0 或更高版本系统的手机上运行则无效,会抛出“Permission Denial”的异常。
接下来,修改 activity 中的代码,修复上面的问题,如下:
public class CallPhoneActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_call_phone);
Button btn_call_phone = (Button) findViewById(R.id.btn_call_phone);
btn_call_phone.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 1. 判断用户是否授权,借助 ContextCompat.checkSelfPermission() 方法
// ContextCompat.checkSelfPermission() 方法接收两参数:context 和权限名
if (ActivityCompat.checkSelfPermission(CallPhoneActivity.this,
Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
// 3. 若没授权,则调用 ActivityCompat.requestPermissions()方法来向用户授权
// 其三个参数:Activity实例、String数组(放权限名)、请求码(只要唯一就行了,这里传1)
ActivityCompat.requestPermissions(CallPhoneActivity.this,
new String[]{Manifest.permission.CALL_PHONE}, 1);
// 4. 调用完 requestPermissions()方法后,会弹出一个权限申请对话框,供用户选择,
// 最后回调 onRequestPermissionsResult()方法
}else {
// 2. 若已授权,则直接执行拨打电话的逻辑
call();
}
}
});
}
/**
* 拨打电话方法
*/
private void call() {
try {
// Intent.ACTION_CALL 是系统内置的打电话动作
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
/**
* 无论用户是否同意权限申请,都会回调此方法
* @param requestCode
* @param permissions
* @param grantResults 授权的结果
*/
@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 {
ToastUtils.showShort("你拒绝了权限请求");
}
break;
default:
break;
}
}
}
这样就完成了权限申请的代码编写了,具体过程看代码注释,现在运行一下程序,效果如下:
好了,关于运行时权限的内容先介绍到这,下面进入主题——内容提供器。
7.2 内容提供器
内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问到另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用内容提供器是 Android 实现跨程序共享数据的标准方式。
不同于文件存储和 SharedPreferences 存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证程序中的隐私数据不会有泄露的风险。
7.2.1 访问其他程序中的数据
内容提供器的用法有两种:
- 使用现有的内容提供器来读取和操作相应程序中的数据
- 创建自己的内容提供器给我们程序的数据提供外部访问接口
7.2.1.1 ContentResolver 的基本用法
每一个应用程序,借助 ContenResolver 类才能访问内容提供器中共享的数据,该类可通过 Context 中的 getContentResolver() 方法获取实例,该类提供的一系列对数据的增删查改方法不同于 SQLiteDataBase,是不接收表名参数的,而是用参数Uri(称为内容 URI) 代替。
内容 URI 给内容提供器中的数据建立了唯一标识符,它由 authority(区分不同的应用程序)和 path(区分不同的表)组成。如某个程序包名为 com.example.app 且存在两张表 table1 和 table2 ,其标准格式写法如下:
在得到了 内容 URI 字符串后,需要调用 Uri.parse() 方法把它解析成 Uri 对象才可作为参数传入,代码如下:
Uri uri = Uri.parse("content://com.example.app.provider/table1")
- 查询操作
现在就可以用这个 Uri 对象来查询 table1 表中的数据了,代码如下:
Curson curson = getContentResolver().query(uri,projection,selection,selectionArgs,sortOrder);
下表对上面的参数进行了详细的解释:
查询完后返回一个 Curson 对象,读取代码如下:
// 通过移动游标的位置来遍历Cursor的所有行
if (cursor != null){
while (cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
- 添加操作
向 table1 表中添加一条数据:
// 将待添加的数据组装到 ContentValues 中
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
// 调用 insert() 方法添加数据
getContentResolver().insert(uri, values);
- 更新操作
更新数据,把 column1 的值清空:
ContentValues values = new ContentValues();
// 清空
values.put("column1","");
// 调用 update() 方法更新数据
getContentResolver().update(uri, values, "column1 = ? and column2 = ?",new String[]{"text","1"});
- 删除操作
调用 ContentResolver 的 delete() 删除数据:
getContentResolver().delete(uri, "column2 = ?", new String[]{"1"});
以上就是 ContentResolver 中的增删查改方法。
7.2.1.2 读取系统联系人
接下来举个例子来加深学习:利用内容提供器来读取系统联系人。
由于模拟器上木有联系人,先向模拟器中创建几个联系人。联系人准备好后,首先在项目的 AndroidManifest.xml 中声明读取联系人的权限:
简单起见,在布局 activity_read_contact.xml 中放一个 ListView 来显示读取的联系人信息:
接着修改 activity 中的代码如下:
public class ReadContactActivity extends AppCompatActivity {
ArrayAdapter adapter;
List contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_read_contact);
// 获取 listView 的实例,设置适配器
ListView contacts_view = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,contactsList);
contacts_view.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 name = cursor.getString(cursor.getColumnIndex
(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
// 获取联系人号码
String number = cursor.getString(cursor.getColumnIndex
(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(name + "\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 {
ToastUtils.showShort("你拒绝了权限请求");
}
break;
default:
break;
}
}
}
运行程序,效果如下:
7.2.2 创建自己的内容提供器
上一小节,介绍了如何访问其他应用程序得数据,其思路是获取应用程序的内容 URI 后借助 ContentResolver 进行 CRUD 操作就行了。接下来介绍创建自己的内容提供器。
7.2.2.1 创建步骤
首先,新建一个类去继承 ContentProvider,重写它的6个抽象方法,如下:
/**
* 自己的内容提供器
* Created by KXwon on 2016/12/18.
*/
public class MyProvider extends ContentProvider{
// 初始化内容提供器的时候调用,返回true表示成功,false失败
@Override
public boolean onCreate() {
return false;
}
// 从内容提供器中查询数据
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
return null;
}
// 向内容提供器中添加一条数据
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
return null;
}
// 从内容提供器中删除数据
@Override
public int delete(Uri uri, String s, String[] strings) {
return 0;
}
// 更新内容提供器中已有的数据
@Override
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
return 0;
}
// 根据传入的内容 URI 来返回 MIME 类型
@Override
public String getType(Uri uri) {
return null;
}
}
之前提到一个标准的内容 URI 写法是这样的:
// 表示调用方期望访问的是 com.example.app 这个应用的 table1 表中的数据
content://com.example.app.provider/table1
除此之外,还可以在内容 URI 后面加一个id,如下:
// 表示调用方期望访问的是 com.example.app 这个应用的 table1 表中 id 为 1 的数据
content://com.example.app.provider/table1/1
内容 URI 的格式主要就只有以上两种,可以通过用通配符的方式来匹配这两种内容 URI,规则如下:
(1) *:表示匹配任意长度的任意字符。
(2)# :表示匹配任意长度的数字
所以,一个能够匹配任意表的内容 URI 格式可写成:
content://com.example.app.provider/*
一个能够匹配 table1 表中任意一行数据的内容 URI 格式可写成:
content://com.example.app.provider/table1/#
然后,再借助 UriMatcher 这个类实现内容 URI 功能,修改 MyProvider 类如下:
public class MyProvider extends ContentProvider{
public static final int TABLE1_DIR = 0; //访问 table1 表中的所有数据
public static final int TABLE1_ITEM = 1;//访问 table1 表中的单条数据
public static final int TABLE2_DIR = 3; //访问 table2 表中的所有数据
public static final int TABLE2_ITEM = 4;//访问 table2 表中的单条数据
private static UriMatcher uriMatcher;
static {
// 创建 UriMatcher 实例
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 调用 addURI() 方法,此方法接收3个参数:authority、path、自定义代码
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[] strings, String s, String[] strings1, String s1) {
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;
}
. . .
}
. . .
}
上述代码只是以 query() 方法做了个示范,其他3个增删改的方法也差不多。
而 getType() 方法,是所有的内容提供器都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型。MIME 字符串主要由 3 部分组成,并有如下格式规定:
(1) 必须由 vnd 开头
(2)若内容 URI 以路径结尾,则后接 android.cursor.dir/,若内容 URI 以 id 结尾,则后接 android.cursor.item/
(3)最后接上 vnd.
最后,实现 getType() 方法中的逻辑,完善 MyProvide r类如下:
public class MyProvider extends ContentProvider{
. . .
// 根据传入的内容 URI 来返回 MIME 类型
@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;
}
}
到这里,一个完整的内容提供器就创建完成了。
7.2.2.1 实现跨程序数据共享
好了,例子来了。上一章项目中建立了 BookStore.db 数据库,里面有 book 表和 category 表这两张表,为简单起见,在上一章的基础上继续开发。
首先,为项目创建个内容提供器,在 Android Studio 中右击 com.wonderful.myfirstcode.chapter7.provider包(你项目所在的包名)→New→Other→Content Provider,会弹出如下窗口:
可以看到,这里把内容提供器命名为 DatabaseProvider,authority 指定为项目包名,Exported 表示是否允许外部程序访问我们的内容提供器,Enabled 表示是否启用这个内容提供器。两个勾选点击 Finish 完成创建。
接着修改 DatebaseProvider 中的代码如下:
public class DataBaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0; //访问 book 表中的所有数据
public static final int BOOK_ITEM = 1;//访问 book 表中的单条数据
public static final int CATEGORY_DIR = 3;
public static final int CATEGORY_ITEM = 4;
public static final String AUTHORITY = "com.wonderful.myfirstcode.chapter7.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
// 创建 UriMatcher 实例
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 调用 addURI() 方法,此方法接收3个参数:authority、path、自定义代码
uriMatcher.addURI(AUTHORITY,"book",BOOK_DIR);
uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM);
uriMatcher.addURI(AUTHORITY,"category",CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY,"category/#",CATEGORY_ITEM);
}
/**
* 初始化内容提供器
*/
@Override
public boolean onCreate() {
// 创建 MyDatabaseHelper 实例
dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db",null,2);
// 返回true表示完成了创建或升级数据库
return true;
}
/**
* 查询数据
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)){
case BOOK_DIR:
// 查询 book 表中的所有数据
cursor = db.query("book",projection,selection,selectionArgs,
null,null,sortOrder);
break;
case BOOK_ITEM:
// 查询 book 表中的单条数据
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 Uri insert(Uri uri, ContentValues values) {
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 int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
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.getReadableDatabase();
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;
}
/**
* 获取 Uri 对象所对应的 MIME 类型
*/
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.wonderful.myfirstcode." +
"chapter7.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.wonderful.myfirstcode." +
"chapter7.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.wonderful.myfirstcode." +
"chapter7.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.wonderful.myfirstcode." +
"chapter7.provider.category";
}
return null;
}
}
另外,内容提供器一定要在 AndroidManifest.xml 中注册才可使用。不过刚用 AS 创建的内容提供器已经帮我们自动注册完成了,如下:
现在,内容提供器已经创建好了,接下来新建一个项目 ProviderTest 来访问上面程序中的数据,记得清空上一章项目里的数据,以防造成干涉。
先来编写下布局文件 activity_main.xml,添加4个按钮来增删查改,如下:
然后修改 MainActivity 中的代码如下:
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 添加数据
Button btn_add_data = (Button) findViewById(R.id.btn_add_data);
btn_add_data.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.wonderful.myfirstcode.chapter7.provider/book");
ContentValues values = new ContentValues();
values.put("name", "第一行代码");
values.put("author", "郭霖");
values.put("pages", 1000);
values.put("price", 66.66);
Uri newUri = getContentResolver().insert(uri,values);
newId = newUri.getPathSegments().get(1);
}
});
// 查询数据
Button btn_query_data = (Button) findViewById(R.id.btn_query_data);
btn_query_data.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.wonderful.myfirstcode.chapter7.provider/book");
Cursor cursor = getContentResolver().query(uri,null,null,null,null);
if (cursor != null){
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex("name"));
String auhtor = cursor.getString(cursor.getColumnIndex("auhtor"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "书名: " + name);
Log.d("MainActivity", "作者: " + auhtor);
Log.d("MainActivity", "页数: " + pages);
Log.d("MainActivity", "价格: " + price);
}
}
cursor.close();
}
});
// 更新数据
Button btn_update_data = (Button) findViewById(R.id.btn_update_data);
btn_update_data.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.wonderful.myfirstcode.chapter7.provider/book/"
+ newId);
ContentValues values = new ContentValues();
values.put("name", "第二行代码");
values.put("pages", 2000);
values.put("price", 88.88);
getContentResolver().update(uri,values,null,null);
}
});
// 删除数据
Button btn_delete_data = (Button) findViewById(R.id.btn_delete_data);
btn_delete_data.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.wonderful.myfirstcode.chapter7.provider/book/"
+ newId);
getContentResolver().delete(uri,null,null);
}
});
}
}
代码很简单,不做解释了,现在运行一下项目,如下界面:
点击 添加数据 按钮,此时数据就添加到上一章项目的数据库中了,点击 查询数据 按钮,打印日志如下:
然后点击 更新数据 按钮,再次点击 查询数据 按钮查看打印日志如下:
最后点击 删除数据 按钮,此时数据就没了,点击 查询数据 按钮也就查询不到数据了。
以上,跨程序共享数据功能成功实现了。
与内容提供器相关内容就介绍到这。下篇文章将进入手机多媒体的学习。