参考书籍:Android第一行代码(第二版).郭霖著
文件存储、SharedPreferences存储及数据库存储所保存的数据只能在当前应用程序中访问(虽然文件存储和SharedPreferences存储中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE操作模式供其它应用程序访问当前应用数据,但在Android4.2中已废弃)。
跨程序数据共享应使用更安全可靠的内容提供器(Android跨程序共享数据的标准方式)。内容提供器提供一套完整机制,允许一个程序访问另一个程序中数据(保证数据安全性),可选择对哪部分数据进行共享(保证隐私不会泄露)。
1、运行时权限
之前的Android权限机制在保护用户安全和隐私方面作用有限。在Android6.0中引入运行时权限功能,更好保护用户安全隐私。
(1)Android权限机制
项目中添加权限声明后,如果用户在低于6.0系统的设备上安装此程序,就会清楚知晓此程序申请了哪些权限从而决定是否安装,但容易造成”店大欺客”的情况。
在6.0系统中加入了运行时权限功能,用户不需在安装软件时一次性授予所有申请的权限,在软件使用过程中可再对某一权限进行授予。
Android将所有权限归成两类:普通权限(不会直接威胁用户安全隐私,自动授权)和危险权限(可能会触及隐私、影响设备安全性,必须用户手动点击授权)。
危险权限:
除以上9组24个权限外都是普通权限(在AndroidManifest.xml中添加权限声明即可)。注意:每个危险权限都属于一个权限组,在进行运行时权限处理时使用的是权限名(用户一旦同意授权,对应权限组中所有其他权限会同时被授权)。
(2)在程序运行时申请权限
新建一个RuntimePermissionTest项目。修改布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call" />
</LinearLayout>
修改MainAcitivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(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 v) {
try {//直接拨打电话,必须声明权限,为防止程序崩溃,所有操作都放在异常捕获代码块中
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
});
}
}
修改AndroidManifest.xml文件,添加:
<uses-permission android:name="android.permission.CALL_PHONE" />
这样在低于Android6.0系统手机上可以运行,但在6.0及更高版本系统运行没有任何效果,会提示权限被禁止的错误信息(必须进行运行时处理)。
修改MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(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 v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
//判断用户是否已给我们授权,第二个参数是具体权限名,使用checkSelfPermission方法的返回值与PackageManager.PERMISSION_GRANTED作比较,相等说明已授权
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE }, 1);
//没授权则需调用ActivityCompat.requestPermissions方法向用户申请授权,第二个参数是String数组,把要申请的权限名放入即可,
// 第三个参数是请求码,只要是唯一值即可
}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();
}
}
//调用requestPermissions()方法后,系统会弹出一个权限申请对话框(用户选择同意或拒绝),不论哪种结果,最终都会回调到onRequestPermissionsResult()方法中
//授权结果会封装在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 {
Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
重新运行程序,点击按钮。
点击DENY:
再次点击按钮,选择ALLOW:
如果再次点击Make Call按钮就不会再弹出权限申请对话框了。如想关闭授权,进入Settings->Apps->RuntimePermissionTest->Permissions,进行关闭即可。
2、访问其他程序中的数据
内容提供器有两种用法:使用现有的内容提供器读取和操作相应程序中数据,创建自己的内容提供器给程序数据提供外部访问接口。
如果应用程序通过内容提供器对其数据提供了外部访问接口,其他任何应用程序都可对这部分数据进行访问。(Android中自带的电话簿、短信、媒体库等都提供了类似的访问接口)
(1)ContentResolver的基本用法
如果要想访问内容提供器中共享的数据,就一定要借助ContentResolver类(通过Context中的getContentResolver方法获取实例)。此类提供了一系列CRUD方法。
不同于SQLiteDatabases,此类中的增删查改方法不接收表名,用Uri参数代替(内容URI)。内容URI由两部分构成:authority(区分不同应用程序,一般用程序包名)和path(区分不同表),如:
content://com.example.app.provider/table1
得到内容URI后,需解析成Uri对象(Uri.parse()方法)才可作为参数传入。可使用此Uri对象查询table1中的数据:
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
仍然返回Cursor对象。
增删改方法类似。
(2)读取系统联系人
需先手动添加几个联系人以便稍后读取。新建一个ContactsTest项目,编写布局文件:
<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" />
</LinearLayout>
为了将关注重点放在读取系统联系人上,没有使用RecyclerView(代码偏多)。修改MainActivity:
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = (ListView) findViewById(R.id.contacts_View);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
contactsView.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);
//ContactsContract.CommonDataKinds.Phone.CONTENT_URI常量为Uri.parse()解析出来的结果
if (cursor != null){
while (cursor.moveToNext()){
//获取联系人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//获取联系人手机号
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
//通知刷新ListView
adapter.notifyDataSetChanged();
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (cursor != null){
//关闭Cursor对象
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 {
Toast.makeText(this, "You denied the permisson", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
还需声明读取系统联系人权限:
运行程序测试。
3、创建自己的内容提供器
(1)创建步骤
新建类继承ContentProvider来创建内容提供器。ContentProvider类中有6个抽象方法,继承时需重写。新建MyProvider:
public class MyProvider extends ContentProvider {
@Override
//初始化内容提供器时调用,通常在此完成对数据库的创建和升级等操作,返回true表示初始化成功
//只有当存在ContentResolver尝试访问程序中数据时,内容提供器才会被初始化
public boolean onCreate() {
return false;
}
@Nullable
@Override
//从内容提供器中查询数据,参数:第一哪张表,第二哪些列,第三第四约束查询哪些行,第五对结果进行排序,结果存在Cursor对象中
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Nullable
@Override
//根据传入的内容URI返回相应的MIME类型
public String getType(Uri uri) {
return null;
}
@Nullable
@Override
//向内容提供器添加一条数据,uri参数确定目标表,新数据保存在values参数中。返回用于表示这条新记录的URI
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
//删除内容提供器中数据。返回被删除的行数
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
//更新内容提供器中已有数据。返回受影响的行数
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
几乎每个方法都有Uri参数(调用ContentResolver的增删查改方法时传递过来的)。
需要对传入的Uri进行解析,分析出调用方期望访问的表和数据。内容URI主要有两种格式,可通过通配符分别匹配:
匹配任意表(*表示匹配任意长度任意字符)
content://com.example.app.provider/*
匹配table1表中任意行数据(#表示匹配任意长度数字)
content://com.example.app.provider/table1/#
接着借助UriMatcher类实现匹配内容URI功能。此类提供了addURI()方法,接收三个参数:authority,path,自定义代码。当调用UriMatcher的match()方法时,将Uri对象闯入,返回值为能匹配这个Uri对象对应的自定义代码(可判断调用方期望访问哪张表中数据)。修改MyProvider:
public class MyProvider extends ContentProvider {
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//将期望匹配的内容URI格式传入
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);
}
...
@Nullable
@Override
//从内容提供器中查询数据,参数:第一哪张表,第二哪些列,第三第四约束查询哪些行,第五对结果进行排序,结果存在Cursor对象中
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
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;
}
...
}
...
以查询方法为例,其他方法类似。getType()用于获取Uri对象对应的MIME类型,MIME字符串主要由三部分组成:
a.以vnd开头
b.如内容URI以路径结尾,后接android.cursor.dir/,如以id结尾,则接android.cursor.item/.
c.最后接上vnd..。
继续实现getType()中逻辑:
...
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;
}
...
(2)跨程序数据共享
在DatabaseTest项目上开发,通过内容提供器加入外部访问接口。首先将MyDatabaseHelper中的Toast去掉(跨程序访问不能使用Toast),然后创建内容提供器,右击包名->New->Other->Content Provider,命名为DatabaseProvider, authority指定为包名.provider.Exported是否允许外部程序访问,Enable是否启用,都勾选。
修改DatabaseProvider:
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.jojo.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);
}
@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;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.jojo.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.jojo.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.jojo.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.jojo.databasetest.provider.category";
default:
break;
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
//添加数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
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() {
//创建MyDatabaseHelper实例,返回true表示内容提供器创建成功,这时数据库已经完成创建升级
dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db", null, 2);
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:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
//getPathSegments将内容URI权限后的部分以“/”符号分割,把分割后的结果放入到一个字符串列表中,
// 此列表的第0个位置存放的就是路径,第1个位置存放的是id
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) {
//更新数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
int updateRows = 0;
switch (uriMatcher.match(uri)){
case BOOK_DIR:
updateRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updateRows = db.update("Book", values, "id = ?", new String[]{bookId});
break;
case CATEGORY_DIR:
updateRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updateRows = db.update("Category", values, "id = ?", new String[]{categoryId});
break;
default:
break;
}
return updateRows;
}
}
内容提供器一定要注册才能使用(Android Studio快捷方式创建的已自动完成这一步)。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.jojo.databasetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.jojo.databasetest.provider"
android:enabled="true"
android:exported="true"></provider>
</application>
</manifest>
现在先将模拟器中的DatabaseTest程序删掉(放置遗留数据造成干扰),然后重新运行安装。关闭掉此项目,新建一个ProviderTest项目(用于访问DatabaseTest中数据)。修改布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/add_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<Button
android:id="@+id/query_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />
<Button
android:id="@+id/update_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />
<Button
android:id="@+id/delete_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />
</LinearLayout>
修改MainActivity:
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//添加数据
Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages",1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = Uri.parse("content://com.example.jojo.databasetest.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 author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//更新刚添加的数据,表中其他行不受影响
Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages",1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
运行程序,分别点击增删改按钮,点击查询按钮查看日志信息。
添加后查询
更新后查询