====================================
====== 第七章:跨程序共享数据 — 探究内容提供器 ======
====================================
Android中前面学的持久化技术:文件存储、SharePreferences存储、数据库存储。这些持久化技术所保存的数据都只能在当前应用程序中访问。
7.1 内容提供器简介:Content Provider
内容提供器主要用于在不同的应用程序之间实现数据共享的功能。它提供了一套机制,允许一个程序访问另一个程序中的数据。同时还能保证被访数据的安全性。
7.2 Android运行时权限
7.2.1 Android权限机制详解:
在manifest中加入相应的权限。在安装的时候可以看到权限。在程序管理界面也可以看到权限。
Android6.0加入了运行时权限功能,也就是说,用户不必要在安装软件的时候一次性授权所有权限,而是可以在软件的使用过程中再对某一项应用申请授权。
Android将所有的权限归类成两类:1、普通权限。2、危险权限。
1、普通权限:不会威胁到用户的安全和隐私的权限,系统会自动帮我们进行授权。比如Broadcast项目中的两个权限
2、危险权限:可能会触及用户隐私,或者对设备安全性造成影响的权限。如获取设备联系人信息。
Android中一共有上百种权限,除了危险权限,其他的都是普通权限:一共有9组共24个危险权限:
1、Calendar:READ_CALENDAR、WRITE_CALENDAR
2、Camera:CAMERA
3、CONTACTS:READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS
4、LOCATION:ACCESS_FINE_LOCATION、ACCESS_CORARSE_LOCATION
5、MICROPHONE:RECORD_AUDIO
6、PHONE:READ_PHONE_STATE、CALL_PHONE、READ_CALL_LOG、WRITE_CALL_LOG、ADD_VOICEMAIL、USE_SIP、PROCESS_OUTGOING_CALLS
7、SENSORS:BODY_SENSORS
8、SMS:SEND_SMS、RECEIVE_SMS、READ_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS
9、STOREAGE:READ_EXTERNAL_STOREAGE、WRITE_EXTERNAL_STORAGE
这9点不必记住,当做是参照表即可。如果属于表中的内容,则需要运行时权限处理,否则,则只需要在manifest职工添加一下权限声明即可。
我们在进行运行时权限处理时使用的是权限名,但是一旦用户同意了授权,那么该权限所对应的权限组中所有的其他权限也会同时被授权。访问http://develop.android.com/reference/android/Mainfest.permission.html可以查看Android系统完整的权限列表。
新建一个RuntimePermissionTest项目。我们先以CALL_PHONE作为例子
在Android6.0之前,打电话其实很简单:
如下:
1、修改activity_main.xml文件
android:layout_width=“match_parent” android:layout_height=“match_parent” />
2、接着在修改MainActivity的代码
public class MainActivity extends AppCompatActivity {
@Override
proected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.id.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse(“tel:10086”);
startActivity(intent);
} catch (SecurityException e) {
e.printStatcTrace();
}
}
});
}
}
我们构建了一个隐式的Intent,intent的action指定为Intent.ACTION_CALL,这是一个打电话的动作。然后在data部分指定了协议是tel,号码是10086。之前我们学过,intent.ACTION_DAIL表示拨号界面,这个是不需要申明权限的。而Intent.ACTION_CALL则可以直接拨打电话,因此必须声明权限,为了防止程序崩溃,我们放在了异常捕获的代码块中。
3、修改AndroidManifest.xml文件:
package=“com.example.runtimepermissiontest” /> android:allowBackup=“true” android:icon=“@mipmap/ic_launcher” android:supportsRtl=“ture” android:theme=“@style/AppTheme” /> 至此,我们已经把拨打电话功能实现了,然后发现在Android6.0以下是可以正常运行的。但是如果我们在6.0以上就会看到错误信息。是因为6.0及以上系统在使用危险权限时候必须进行运行时权限处理: 1、修改MainActivity代码: public class MainActivity extends AppCompatActivity { @Override protected void onCreate(savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.id.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, Mainifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermission(MainActivity.this, new String[] {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 onRequestPermissionResult(int requestCode, String[] permissions, 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: break; } } } 说白了,运行时权限的核心就是在程序运行时过程中由用户授权去执行某些危险的操作。借助ContextCompat.checkSelfPermission()方法,接收两个参数,第一个参数是Context,第二个参数是具体的权限名,比如打电话的权限名,如Mainfest.permission.CALL_PHONE,然后我们使用方法的返回值和PackageManager.PERMISSION_GRANDED做比较,相等就说明用户已经授权,不等就表示用户没有授权。 requestPermissions()方法接收三个参数:1、Activity的实例。2、String数组,我们把要申请的权限名放在数组中即可。3、请求码,只要是唯一值即可。 7.3 访问其他程序中的数据: 内容提供器的用法有两个: 1、使用现有的内容提供器来读取和操作相应程序中的数据。 2、创建自己的内容提供器给我们的程序的数据提供外部访问接口。 如果一个程序通过内容提供器对其数据提供了外部的访问接口,那么任何其他的应用程序都可以对这部分数据进行访问。 ContentResolver的基本用法: 对于每一个应用程序,想要访问内容选择器中共享的数据,一定要借助ContentResolver,可以通过Context中的getContetnResolver()方法获取该类的实例。 ContentResolver提供了insert()、update()、delete()、query()方法来进行CRUD操作。 ContentResolver的其中一个参数是Uri,这个参数被成为内容URI,它主要由两个部分组成:authority和path authority用于对不同的引用程序做区分,一般为了避免冲突,用包名命名:如com.jifenzhi.test path用于对同一应用程序不同的表进行区分。通常会添加到authority的后面。/table1 内容Uri一个例子就是com.jifenzhi.test/table1 协议为content:// 所以,一个标准的URI如下所示:content://com.jifenzhi.test/table1 Uri uri = Uri.parse(“content://com.jifenzhi.test/table1”); 现在我们可以通过Uri对象来查询table1表中的数据了。 1、查询操作 Cursor cursor = getContentResolver().query ( uri, projection, selection, selectionArgs, sortOrder); 上面query的参数和SQLiteDatabase中query()方法的参数很像, query()方法参数 对应SQL部分 描述 uri from table_name 指定查询某个应用程序下的某一张表 projection select column1,column2 指定查询的列名 selection where column = value 指定where的约束条件 selectionArgs - 为where中的占位符提供具体的值 orderBy order by column1, column2 指定查询结果的排序方式 处理cursor的代码如下: if (cursor != null) { while (cursor.moveToNext()) { String column1 = cursor.getString(cursor.getColumnIndex(“column1”); int column2 = cursor.getInt(cursor.getColumnIndex(“column2”)); } cursor.close(); } 2、插入操作 ContentValues values = new ContentValues(); values.put(“column1”, “text”); values.put(“column2”, 1); getContentResolver().insert(uri, values); 3、更新操作 ContentValues values = new ContentValues(); values.put(“column1”, “”); getContentResolver().update(uri, values, “column1 = ? and column2 = ?”, new Sting[] {“text”, “1”}); 上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束。以防所有的行都会受影响。 4、删除操作; getContentResolver().delete(uri, “column2 = ?”. new String[] {“1”} ); 7.3.2 读取系统联系人功能: 创建一个ContactTest项目: 1、修改activity_main.xml文件 android:layout_width=“match_parent” android:layout_height=“match_parent” android:orientation=“vertical” /> android:id=“@+id/contacts_view” android:layout_width=“match_parent” android:layout_height=“match_parent” /> 2、修改MainActivity代码: public class MainActivity extends AppCompatActivity { ArrayAdapter List @Override protected void onCreate(savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.id.activity_main); contactsView = (ListView) findViewById(R.id.contacts_view); adapter = new ArrayAdapter contactsView.setAdapter(adapter); if (ContextCompat.checkSelfPermission(this, Mainifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANDED) { 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 displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); // 获取联系人手机号 String number = cursor.getString(cursor.getColumnIndex(ContactContract.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, String[] permissions, int[] grantResults) { switch (requestCode) { case 1: if (grantResults.length > 0 && grantResult[0] == PackagerManger.PERMISSION_GRANTED) { readContacts(0; } else { Toast.makeText(this, “You denied the permission”, Toast.LENGTH_SHORT).show(); } break; default: } } } 联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPALY_NAME 联系人手机号这一列的常量是ContactsContract.CommonDataKinds.Phone.NUMBER 不要忘记在最后使用完cursor之后关闭他 3、在AndroidManifest.xml中添加声明 package=“com.example.contactstest” > 加入这个android.permission.READ_CONTACTS权限,我们的程序就可以访问到系统的联系人数据了。 7.4 创建自己的内容选择器; 前面我们学习了如何在自己的程序中访问其他应用程序的数据。总体来说就是,获取到应用程序的内容URI,然后借助ContentResolver进行CRUD操作就可以了。 那么,那些提供外部访问接口的应用程序是如何实现这些功能的呢?又是如何保证数据的安全性呢? 7.4.1 创建内容提供器的步骤: 1、通过新建一个类去集成ContentProvider,ContentProvider类中有6个抽象方法。需要全部重写: public class MyProvider extends ContentProvider { @Override public boolean onCreate() { return false; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public int delete(Uri uri, String selction, String[] selctionArgs) { return 0; } @Override public String getType(Uri uri) { return null; } } 1、onCreate()方法: 初始化内容提供器的时候调用,通常会在这里完成对数据库的创建和升级操作,返回true表示初始化成功,false表示失败。只有当ContentResolver尝试访问我们的数据时,内容提供器才会被初始化。 2、query()方法: 从内容选择器中查询数据,用uri来确定要查的是那张表,projection用来确认查询哪些列,selection和selectionArgs用来约束查询哪些行,sortOrder用来对结果进行排序,查询的结果作为Cursor对象返回。 3、insert()方法: uri用来确认要添加到的表,values表示待添加的数据。返回一条用于表示这条新纪录的URI 4、update()方法: 其他参数类似。受影响的行数将作为范慧慧返回。 5、delete()方法: 其他参数类似。被删除的行数将作为返回值返回。 6、getType()方法。 根据传入的Uri来返回相应的MIME类型。 content://com.example.app.provider/table1/1 表示访问table1表的id为1的数据。 *:表示匹配任意长度的任意字符 如匹配任意表的内容:content://com.example.text/* 如匹配table表中任意一行数据的内容的URI:content://com.example.test/table1/# 接着,我们借助UriMatcher这个类就可以轻松实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,方法接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将Uri对象传入,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据。(看代码后的理解:第三个参数就是用于当使用了UriMatcher的match方法来匹配uri,会返回一个结果,结果就是第三个参数。) 修改MyProvider代码: public class MyProvider extends ContentProvider { public static final int TABEL1_DIR = 0; public static final int TABLE1_ITEM = 1; public static final int TABLE2_DIR = 2; public static final int TABLE2_ITEM = 3; public static UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 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”, TABLE_DIR); uriMatcher.addURI(“com.example.app.provider”, “table2/#”, TABLE2_ITEM); } @Override 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类型。 一个内容URI所对应的MIME字符串主要由3部分组成。,Android对这3部分做了如下格式规定: 1、必须以vnd开头。 2、如果内容URI以路径结尾,则后面接andrdoi.cursor.dir/,如果内容URI以id结尾,则后面接android.cursor.item/ 3、最后接上vnd. 所以,对于content://com.exapmle.app.provider/table1这个内容URI。它所对应的MIME类型可以写成: vnd.android.cursor.dir/vnd.com.example.app.provider 接下来我们实现MyProvider中getTYpe()方法的逻辑: @Override public String getType() { switch (uriMatcher.match(uri) { case TABLE1_DIR: return “vnd.android.cursor.dir/vnd.com.example.app.provider.table1”; break; case TABLE1_ITEM: return …. 现在,任何一个应用程序都可以使用ContentResolver来访问我们程序中的数据了。并且保证了隐私数据不会泄露,这对亏了内容提供器的良好机制,所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行的。而我们当然不可能向UriMatcher中添加隐私数据的URI。 7.4.2 实现跨程序数据共享: 1、创建一个内容提供器: 右键com.example.broadcasttest包 —> New —> Other —> Content Provider,并取名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 CATEGOTY_ITEM = 3; public static final String AUTHORITY = “com.example.databasetest.provider”; private state 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, “categoty”, CATEGOTY_DIR); uriMatcher.addURI(AUTHORITY, “categoty/#”, CATEGOTY_ITEM); } @Override public boolean onCreate() { 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: String bookId = uri.getPathSegments().get(1); cursor = db.query(“Book”, projection, “id = ?”, new String[] { bookId }, null, null, sortOrder); break; case CATEGOTY_DIR: cursor = db.query(“Categoty”, projection, selection, selectionArgs, null, null, sortOrder); break; case CATEGOTY_ITEM: String categotyId = uri.getPathSegments().get(1); cursor = db.query(“Categoty”, projection, “id = ?”, new String[] {categotyId}, null, null, sortOrder); break; default: break; } return cursor; } @Override public Uri insert(Uri uri, ContentValues values) { // 添加数据 SQLIteDatabase db = dbHelper.getWriteableDatabase(); Uri uriReturn = null; swith (uriMatcher.match(uri) { case BOOK_DIR: case BOOK_ITEM: 调用了Uri对象的getPathSegments()方法, 内容提供器一定要在AndroidManifest.xml文件中注册才可以使用。 7.5 git进阶: Android Studio在我们创建的时候会自动创建两个.gitignore文件。一个在根目录下,一个在app模块下面。 git status:查看文件修改情况 git diff:查看文件的更改内容 git diff app/scr/main/java/com/example/providertest/MainActivity.java:表示匹配任意长度的数字