Android中包含了大量的应用程序,那么如何获取其它应用程序的数据,来做一些更有意思的事情呢?内容提供作为Android的四大组件之一就是用来实现应用程序之间数据共享的。
内容提供器是实现Android跨程序之间数据共享的一种安全方式。前面我们讲到Android持久化技术,我们也可以让其它应用程序直接访问我们的持久化文件实现数据共享,但是这种方式存在安全隐患,因此又提出了内容提供器技术方案,通过内容提供器可以控制开发哪些数据让别的程序访问,进一步提高数据共享的安全性。
内容提供器是实现跨程序数据共享,既然是访问别的程序数据,为了安全性起见,自然不是你想访问谁的数据就访问谁的数据,你需要先申请访问该程序的权限,然后由用户决定是否同意你的申请,只有权限申请通过了,才能利用内容提供器访问程序中的数据,而运行时权限就是实现这一过程的方案。
申请的权限包括普通权限和危险权限,普通权限只需要在AndroidManifest.xml文件中声明该权限即可,由系统自动授权;危险权限不仅需要在AndroidManifest.xml文件中声明该权限,而且需要在代码中申请该权限,然后由用户决定是否授权。
Android中涉及的危险权限如下表所示:
AndroidManifest.xml文件配置:
代码申请权限:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED)
{//没有读取电话簿的权限,需要代码申请权限,参数1:context 参数2:需要申请的权限列表 参数3:申请权限的标识,在申请结果的回调函数中使用ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
}
else//有读取电话簿的权限,可以进行读取电话簿数据readContacts();
@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();//如果用户没有同意申请,进行其它逻辑elseToast.makeText(this, "未授予读取电话本权限", Toast.LENGTH_SHORT).show();break;default:break;}}
Android提供ContentResolver类来获取其它应用程序中的数据,ContentResolver的对象通过Context的getContentResolver方法获得。ContentResolver包含操作数据库的常用方法如query,insert,delete,update等等。
我们知道操作数据库首先需要确定是操作数据库中的哪张表,通常我们直接通过表名来确定是哪张表。
而访问其它程序的数据,首先要确定访问的是哪个程序,然后才能确定是访问的哪张表,因此Android提供一个内容URI来内容提供器中的数据建立唯一的标准,简单来说URI就是指定哪个程序中的哪张表的唯一标识。
答疑:URI中不需要指定是哪个数据库,因为开放内容提供器接口的程序内部已经设定了是哪个数据库,访问方只需要确定是哪张表就可以了,这个在下一节我们创建自己的内容提供器的时候你就能看到。
URI的格式为:content://authority和path
ContentResolver contentResolver = getContentResolver();
//参数1:唯一标识要查询的是哪个程序下的哪张表; 参数2:指定查询的列名
//参数3:指定where的约束条件; 参数4:为参数3的占位符提供具体的值
//参数5:对查询结果的排序方式
contentResolver.query(uri,projection,selection,selectionArgs,sortOrder);
ContentResolver contentResolver = getContentResolver(# 参考书籍:第一行代码
链接:https://pan.baidu.com/s/1aXtOQCXL6qzxEFLBlqXs1Q?pwd=n5ag
提取码:n5ag);
//参数1:URI 参数2:待插入数据的键值对值
contentResolver.insert(uri,ContentValues)
ContentResolver contentResolver = getContentResolver();
//参数1:URI 参数2:待插入数据的键值对值 参数3和参数4约束更新范围
contentResolver.update(uri,ContentValues,selection,selectionArgs)
ContentResolver contentResolver = getContentResolver();
//参数1:URI; 参数2和参数3约束删除范围
contentResolver.delete(uri,selection,selectionArgs)
Cursor cursor=null;try {ContentResolver contentResolver = getContentResolver();//电话本程序的URI由ContactsContract.CommonDataKinds.Phone封装好了,我们直接用可以了cursor= contentResolver.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));}}}catch (Exception e){e.printStackTrace();}finally {if (cursor!=null)cursor.close();}
android:authorities="com.xiaomi.databasetest.provider" android:enabled="true"android:exported="true" />
答疑:
UriMatcher对象添加URI的方法是addURI(String authority, String path, int code),authority和path在前面已经讲过,code是当前匹配URI的唯一标识。
上面讲到path代表的是表名,例如/table1,其实表名后面还可以加一个数字,代码这个表的ID,即访问表中的第几行数据,例如/table1/2。
public class MyProvider extends ContentProvider {//自定义code,指定URI的唯一标识public static final int TABLE1_DIR=0;public static final int TABLE1_ITEM=1;private static UriMatcher uriMatcher;static {//正则表达式: *代码任意长度的字符,#代表任意长度的数字uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);//将content://com.xiaomi.app.provider/table1添加到Matcher对象中,代码想要把table1整张表给其它程序访问uriMatcher.addURI("com.xiaomi.app.provider","table1",TABLE1_DIR);//将content://com.xiaomi.app.provider/table1/#添加到Matcher对象中,代码想要把table1表中的指定行给其它程序访问uriMatcher.addURI("com.xiaomi.app.provider","table1/#",TABLE1_ITEM);}@Overridepublic boolean onCreate() {//指定或者创建想要共享数据的数据库(这儿也能解释为什么URI中不需要指定数据库的原因了)return false;}@Nullable@Override//提供给其它程序查询本程序数据库的方法public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {switch (uriMatcher.match(uri)){case TABLE1_DIR:System.out.printf("向外部提供table1表下的所有数据");break;case TABLE1_ITEM:System.out.printf("向外部提供table1表下的单条数据");break;default:break;}return null;}@Nullable@Overridepublic Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {return null;}@Overridepublic int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {return 0;}@Overridepublic int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs) {return 0;}@Nullable@Overridepublic String getType(@NonNull Uri uri) {//获取URI对应的MIME字符串//URI以路径结尾,MIME格式:vnd.android.cursor.dir/vnd..//URI以id结尾,MIME格式:vnd.android.cursor.item/vnd..switch (uriMatcher.match(uri)){case TABLE1_DIR:return "vnd:android.cursor.dir/vnd.com.xiaomi.app.provider.table1";case TABLE1_ITEM:return "vnd:android.cursor.item/vnd.com.xiaomi.app.provider.table1";default:break;}return null;}}
答疑:
创建的自己的内容提供器除了实现增删改查等操作数据库的方法外,还需要实现getType方法,该方法是根据传入的URI来返回相应的MIME类型。
一个URI对应的MIME字符串主要由一下三部分组成:
示例:
URI:content://com.example.app.provider/table1
对应MIME:vnd.android.cursor.dir/vnd.com.example.app.provider.table1
URI:content://com.example.app.provider/table1/3
对应MIME:vnd.android.cursor.item/vnd.com.example.app.provider.table1
public class MyDatabaseHelper extends SQLiteOpenHelper {private Context mContext;public static final String CREATE_BOOK_TABLE="create table Book(" +"id integer primary key autoincrement," +"author text," +"price real," +"pages integer," +"name text)";public static final String DROP_BOOK="drop table if exists Book";public static final String CREATE_CATEGORY_TABLE="create table Category(" +"id integer primary key autoincrement," +"category_name text," +"category_code integer)";public static final String DROP_CATEGORY="drop table if exists Category";public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {super(context, name, factory, version);this.mContext=context;}//数据库不存在的前提下,创建数据库完成后会调用该方法@Overridepublic void onCreate(SQLiteDatabase sqLiteDatabase) {sqLiteDatabase.execSQL(CREATE_BOOK_TABLE);sqLiteDatabase.execSQL(CREATE_CATEGORY_TABLE);//新增这张表//Toast.makeText(mContext, "相关表创建完成", Toast.LENGTH_SHORT).show();}//数据库版本发生变化的时候,创建数据库操作后,会调用该方法@Overridepublic void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {sqLiteDatabase.execSQL(DROP_BOOK);sqLiteDatabase.execSQL(DROP_CATEGORY);onCreate(sqLiteDatabase);}
}
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;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.xiaomi.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() {}@Overridepublic boolean onCreate() {// TODO: Implement this to initialize your content provider on startup.dbHelper=new MyDatabaseHelper(getContext(),"BookStore.db",null,1);return true;}@Overridepublic Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {// TODO: Implement this to handle query requests from clients.Cursor cursor=null;SQLiteDatabase database = dbHelper.getReadableDatabase();switch (uriMatcher.match(uri)){case BOOK_DIR:cursor=database.query("Book",projection,selection,selectionArgs,null,null,sortOrder);break;case BOOK_ITEM:String bookID = uri.getPathSegments().get(1);cursor=database.query("Book",projection,"id = ?",new String[]{bookID},null,null,sortOrder);break;case CATEGORY_DIR:cursor=database.query("Category",projection,selection,selectionArgs,null,null,sortOrder);break;case CATEGORY_ITEM:String categoryID = uri.getPathSegments().get(1);cursor=database.query("Category",projection,"id = ?",new String[]{categoryID},null,null,sortOrder);break;default:break;}return cursor;}@Overridepublic Uri insert(Uri uri, ContentValues values) {// TODO: Implement this to handle requests to insert a new row.SQLiteDatabase writableDatabase = dbHelper.getWritableDatabase();Uri resUri=null;long newID=-1;switch (uriMatcher.match(uri)){case BOOK_DIR:case BOOK_ITEM:newID = writableDatabase.insert("Book", null, values);resUri=Uri.parse("content://"+AUTHORITY+"/book/"+newID);break;case CATEGORY_DIR:case CATEGORY_ITEM:newID = writableDatabase.insert("Category", null, values);resUri=Uri.parse("content://"+AUTHORITY+"/category/"+newID);break;default:break;}return resUri;}@Overridepublic int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) {// TODO: Implement this to handle requests to update one or more rows.SQLiteDatabase writableDatabase = dbHelper.getWritableDatabase();int updatedRows=0;switch (uriMatcher.match(uri)){case BOOK_DIR:updatedRows = writableDatabase.update("Book", values, selection, selectionArgs);break;case BOOK_ITEM:String bookID = uri.getPathSegments().get(1);updatedRows = writableDatabase.update("Book", values, "id = ?", new String[]{bookID});break;case CATEGORY_DIR:updatedRows = writableDatabase.update("Category", values, selection, selectionArgs);break;case CATEGORY_ITEM:String categoryID = uri.getPathSegments().get(1);updatedRows = writableDatabase.update("Category", values, "id = ?", new String[]{categoryID});break;default:break;}return updatedRows;}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) {// Implement this to handle requests to delete one or more rows.SQLiteDatabase writableDatabase = dbHelper.getWritableDatabase();int deleteRows=0;switch (uriMatcher.match(uri)){case BOOK_DIR:deleteRows = writableDatabase.delete("Book", selection, selectionArgs);break;case BOOK_ITEM:String bookID = uri.getPathSegments().get(1);deleteRows=writableDatabase.delete("Book","id = ?",new String[]{bookID});break;case CATEGORY_DIR:deleteRows = writableDatabase.delete("Category", selection, selectionArgs);break;case CATEGORY_ITEM:String categoryID = uri.getPathSegments().get(1);deleteRows=writableDatabase.delete("Category","id = ?",new String[]{categoryID});break;default:break;}return deleteRows;}@Overridepublic 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.xiaomi.databasetest.provider.book";case BOOK_ITEM:return "vnd.android.cursor.item/vnd.com.xiaomi.databasetest.provider.book";case CATEGORY_DIR:return "vnd.android.cursor.dir/vnd.com.xiaomi.databasetest.provider.category";case CATEGORY_ITEM:return "vnd.android.cursor.item/vnd.com.xiaomi.databasetest.provider.category";}return null;}# 参考书籍:第一行代码
链接:https://pan.baidu.com/s/1aXtOQCXL6qzxEFLBlqXs1Q?pwd=n5ag
提取码:n5ag
}
ContentResolver contentResolver = getContentResolver();ContentValues contentValues = new ContentValues();
contentValues.put("name","这个世界真是很美好呀");
contentValues.put("author","从外部程序来的作者");
contentValues.put("pages",666);
contentValues.put("price",19.99);Uri uri = Uri.parse("content://com.xiaomi.databasetest.provider/book");
Uri newUri = contentResolver.insert(uri, contentValues);
String newID = newUri.getPathSegments().get(1);//返回的新插入数据的id
Toast.makeText(MainActivity.this, "添加数据成功", Toast.LENGTH_SHORT).show();
Uri uri = Uri.parse("content://com.xiaomi.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor==null)
{Toast.makeText(MainActivity.this, "未查询到数据", Toast.LENGTH_SHORT).show();return;
}
if (cursor.moveToFirst())
{StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("name"+" "+"author"+" "+"pages"+" "+"price\r\n");do {String name = cursor.getString(cursor.getColumnIndex("name"));String author = cursor.getString(cursor.getColumnIndex("author"));int pages = cursor.getInt(cursor.getColumnIndex("pages"));double price = cursor.getDouble(cursor.getColumnIndex("price"));stringBuilder.append(name+" "+author+" "+pages+" "+price+"\r\n");
}while (cursor.moveToNext());
Uri uri = Uri.parse("content://com.xiaomi.databasetest.provider/book/"+newID);
ContentValues contentValues = new ContentValues();
contentValues.put("price","26.66");
contentValues.put("name","这个世界多么美好");
getContentResolver().update(uri,contentValues,null,null);
Uri uri = Uri.parse("content://com.xiaomi.databasetest.provider/book/"+newID);
getContentResolver().delete(uri,null,null);
链接:https://pan.baidu.com/s/1aXtOQCXL6qzxEFLBlqXs1Q?pwd=n5ag
提取码:n5ag