4.3.1 创建/使用内容供应器 示例代码
原书:Android Application Secure Design/Secure Coding Guidebook
译者:飞龙
协议:CC BY-NC-SA 4.0
使用内容供应器的风险和对策取决于内容供应器的使用方式。 在本节中,我们根据内容供应器的使用方式,对 5 种类型的内容供应器进行了分类。 您可以通过下面显示的图表,找出您应该创建哪种类型的内容供应器。
表 4.3-1 内容供应器类型定义
| 类型 |
定义 |
| 私有 |
不能由其他应用使用的内容供应器,所以是最安全的 |
| 公共 |
应该由未指定的大量应用使用的内容供应器 |
| 伙伴 |
只能由可信的伙伴公司开发的特定应用使用的内容供应器 |
| 内容 |
只能由其它内部应用使用的内容供应器 |
| 临时 |
基本上是私有内容供应器,但允许特定应用访问特定 URI |
![]()
4.3.1.1 创建/使用私有内容供应器
私有内容供应器是只由单一应用使用的内容提供者,它是最安全的内容供应器 [8]。
下面展示了如何实现私有内容供应器的示例代码。
要点(创建内容供应器):
1) 将导出属性显式设置为false。
2) 即使数据来自相同应用,也应该小心并安全地处理收到的请求数据。
3) 可以发送敏感信息,因为它在同一应用内发送和接收所有信息。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.provider.privateprovider">
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".PrivateUserActivity"
android:label="@string/app_name"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name=".PrivateProvider"
android:authorities="org.jssec.android.provider.privateprovider"
android:exported="false" />
</application>
</manifest>
PrivateProvider.java
package org.jssec.android.provider.privateprovider;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
public class PrivateProvider extends ContentProvider {
public static final String AUTHORITY = "org.jssec.android.provider.privateprovider";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.org.jssec.contenttype";
public interface Download {
public static final String PATH = "downloads";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
public interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static final int DOWNLOADS_CODE = 1;
private static final int DOWNLOADS_ID_CODE = 2;
private static final int ADDRESSES_CODE = 3;
private static final int ADDRESSES_ID_CODE = 4;
private static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
}
private static MatrixCursor sAddressCursor = new MatrixCursor(new String[] { "_id", "city" });
static {
sAddressCursor.addRow(new String[] { "1", "New York" });
sAddressCursor.addRow(new String[] { "2", "Longon" });
sAddressCursor.addRow(new String[] { "3", "Paris" });
}
private static MatrixCursor sDownloadCursor = new MatrixCursor(new String[] { "_id", "path" });
static {
sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case ADDRESSES_CODE:
return CONTENT_TYPE;
case DOWNLOADS_ID_CODE:
case ADDRESSES_ID_CODE:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case DOWNLOADS_ID_CODE:
return sDownloadCursor;
case ADDRESSES_CODE:
case ADDRESSES_ID_CODE:
return sAddressCursor;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return ContentUris.withAppendedId(Download.CONTENT_URI, 3);
case ADDRESSES_CODE:
return ContentUris.withAppendedId(Address.CONTENT_URI, 4);
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 5;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 15;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 10;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 20;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
}
下面是活动的示例,它使用私有内容供应器。
要点(使用内容供应器):
4) 敏感信息可以发送,因为目标供应器在相同应用中。
5) 小心和安全地处理收到的结果数据,即使数据来自相同应用。
PrivateUserActivity.java
package org.jssec.android.provider.privateprovider;
import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class PrivateUserActivity extends Activity {
public void onQueryClick(View view) {
logLine("[Query]");
Cursor cursor = null;
try {
cursor = getContentResolver().query(
PrivateProvider.Download.CONTENT_URI, null, null, null, null);
if (cursor == null) {
logLine(" null cursor");
} else {
boolean moved = cursor.moveToFirst();
while (moved) {
logLine(String.format(" %d, %s", cursor.getInt(0), cursor.getString(1)));
moved = cursor.moveToNext();
}
}
}
finally {
if (cursor != null) cursor.close();
}
}
public void onInsertClick(View view) {
logLine("[Insert]");
Uri uri = getContentResolver().insert(PrivateProvider.Download.CONTENT_URI, null);
logLine(" uri:" + uri);
}
public void onUpdateClick(View view) {
logLine("[Update]");
int count = getContentResolver().update(PrivateProvider.Download.CONTENT_URI, null, null, null);
logLine(String.format(" %s records updated", count));
}
public void onDeleteClick(View view) {
logLine("[Delete]");
int count = getContentResolver().delete(
PrivateProvider.Download.CONTENT_URI, null, null);
logLine(String.format(" %s records deleted", count));
}
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}
4.3.1.2 创建/使用公共内容供应器
公共内容供应器是应该由未指定的大量应用使用的内容供应器。 需要注意的是,由于它不指定客户端,它可能会受到恶意软件的攻击和篡改。 例如,可以通过select()获取保存的数据,可以通过update()更改数据,或者可以通过insert()/ delete()插入/删除假数据。
另外,在使用 Android OS 未提供的自定义公共内容供应器时,需要注意的是,恶意软件可能会接收到请求参数,伪装成自定义公共内容供应器,并且也可能发送攻击数据。 Android OS 提供的联系人和 MediaStore 也是公共内容提供商,但恶意软件不能伪装成它们。
实现公共内容供应器的样例代码展示在下面:
要点(创建内容供应器):
1) 将导出的属性显式设置为true。
2) 仔细安全地处理收到的请求数据。
3) 返回结果时,请勿包含敏感信息。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.provider.publicprovider">
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<provider
android:name=".PublicProvider"
android:authorities="org.jssec.android.provider.publicprovider"
android:exported="true" />
</application>
</manifest>
PublicProvider.java
package org.jssec.android.provider.publicprovider;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
public class PublicProvider extends ContentProvider {
public static final String AUTHORITY = "org.jssec.android.provider.publicprovider";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.org.jssec.contenttype";
public interface Download {
public static final String PATH = "downloads";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
public interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static final int DOWNLOADS_CODE = 1;
private static final int DOWNLOADS_ID_CODE = 2;
private static final int ADDRESSES_CODE = 3;
private static final int ADDRESSES_ID_CODE = 4;
private static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
}
private static MatrixCursor sAddressCursor = new MatrixCursor(new String[] { "_id", "city" });
static {
sAddressCursor.addRow(new String[] { "1", "New York" });
sAddressCursor.addRow(new String[] { "2", "London" });
sAddressCursor.addRow(new String[] { "3", "Paris" });
}
private static MatrixCursor sDownloadCursor = new MatrixCursor(new String[] { "_id", "path" });
static {
sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case ADDRESSES_CODE:
return CONTENT_TYPE;
case DOWNLOADS_ID_CODE:
case ADDRESSES_ID_CODE:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case DOWNLOADS_ID_CODE:
return sDownloadCursor;
case ADDRESSES_CODE:
case ADDRESSES_ID_CODE:
return sAddressCursor;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return ContentUris.withAppendedId(Download.CONTENT_URI, 3);
case ADDRESSES_CODE:
return ContentUris.withAppendedId(Address.CONTENT_URI, 4);
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 5;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 15;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 10;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 20;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
}
下面是使用公共内容供应器的活动示例。
要点(使用内容供应器):
4) 不要发送敏感信息
5) 收到结果时,小心和安全地处理结果数据
PublicUserActivity.java
package org.jssec.android.provider.publicuser;
import android.app.Activity;
import android.content.ContentValues;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class PublicUserActivity extends Activity {
private static final String AUTHORITY = "org.jssec.android.provider.publicprovider";
private interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
public void onQueryClick(View view) {
logLine("[Query]");
if (!providerExists(Address.CONTENT_URI)) {
logLine(" Content Provider doesn't exist.");
return;
}
Cursor cursor = null;
try {
cursor = getContentResolver().query(Address.CONTENT_URI, null, null, null, null);
if (cursor == null) {
logLine(" null cursor");
} else {
boolean moved = cursor.moveToFirst();
while (moved) {
logLine(String.format(" %d, %s", cursor.getInt(0), cursor.getString(1)));
moved = cursor.moveToNext();
}
}
}
finally {
if (cursor != null) cursor.close();
}
}
public void onInsertClick(View view) {
logLine("[Insert]");
if (!providerExists(Address.CONTENT_URI)) {
logLine(" Content Provider doesn't exist.");
return;
}
ContentValues values = new ContentValues();
values.put("city", "Tokyo");
Uri uri = getContentResolver().insert(Address.CONTENT_URI, values);
logLine(" uri:" + uri);
}
public void onUpdateClick(View view) {
logLine("[Update]");
if (!providerExists(Address.CONTENT_URI)) {
logLine(" Content Provider doesn't exist.");
return;
}
ContentValues values = new ContentValues();
values.put("city", "Tokyo");
String where = "_id = ?";
String[] args = { "4" };
int count = getContentResolver().update(Address.CONTENT_URI, values, where, args);
logLine(String.format(" %s records updated", count));
}
public void onDeleteClick(View view) {
logLine("[Delete]");
if (!providerExists(Address.CONTENT_URI)) {
logLine(" Content Provider doesn't exist.");
return;
}
int count = getContentResolver().delete(Address.CONTENT_URI, null, null);
logLine(String.format(" %s records deleted", count));
}
private boolean providerExists(Uri uri) {
ProviderInfo pi = getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
return (pi != null);
}
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}
4.3.1.3 创建/使用伙伴内容供应器
合作伙伴内容供应器只能由特定应用使用。 该系统由伙伴公司的应用和内部应用组成,用于保护在伙伴应用和内部应用之间处理的信息和功能。
下面显示了用于实现伙伴内容供应器的示例代码。
要点(创建内容供应器):
1) 将导出属性显式设置为true。
2) 验证请求应用的证书是否已在自己的白名单中注册。
3) 即使数据来自伙伴应用,也要小心并安全地处理收到的请求数据。
4) 可以返回开放给伙伴应用的信息。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.provider.partnerprovider">
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<provider
android:name=".PartnerProvider"
android:authorities="org.jssec.android.provider.partnerprovider"
android:exported="true" />
</application>
</manifest>
PartnerProvider.java
package org.jssec.android.provider.partnerprovider;
import java.util.List;
import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
public class PartnerProvider extends ContentProvider {
public static final String AUTHORITY = "org.jssec.android.provider.partnerprovider";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.org.jssec.contenttype";
public interface Download {
public static final String PATH = "downloads";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
public interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static final int DOWNLOADS_CODE = 1;
private static final int DOWNLOADS_ID_CODE = 2;
private static final int ADDRESSES_CODE = 3;
private static final int ADDRESSES_ID_CODE = 4;
private static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
}
private static MatrixCursor sAddressCursor = new MatrixCursor(new String[] { "_id", "city" });
static {
sAddressCursor.addRow(new String[] { "1", "New York" });
sAddressCursor.addRow(new String[] { "2", "London" });
sAddressCursor.addRow(new String[] { "3", "Paris" });
}
private static MatrixCursor sDownloadCursor = new MatrixCursor(new String[] { "_id", "path" });
static {
sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
}
own white list.
private static PkgCertWhitelists sWhitelists = null;
private static void buildWhitelists(Context context) {
boolean isdebug = Utils.isDebuggable(context);
sWhitelists = new PkgCertWhitelists();
sWhitelists.add("org.jssec.android.provider.partneruser", isdebug ?
"0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
"1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");
}
private static boolean checkPartner(Context context, String pkgname) {
if (sWhitelists == null) buildWhitelists(context);
return sWhitelists.test(context, pkgname);
}
private String getCallingPackage(Context context) {
String pkgname;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
pkgname = super.getCallingPackage();
} else {
pkgname = null;
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> procList = am.getRunningAppProcesses();
int callingPid = Binder.getCallingPid();
if (procList != null) {
for (RunningAppProcessInfo proc : procList) {
if (proc.pid == callingPid) {
pkgname = proc.pkgList[proc.pkgList.length - 1];
break;
}
}
}
}
return pkgname;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case ADDRESSES_CODE:
return CONTENT_TYPE;
case DOWNLOADS_ID_CODE:
case ADDRESSES_ID_CODE:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
throw new SecurityException("Calling application is not a partner application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case DOWNLOADS_ID_CODE:
return sDownloadCursor;
case ADDRESSES_CODE:
case ADDRESSES_ID_CODE:
return sAddressCursor;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
throw new SecurityException("Calling application is not a partner application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return ContentUris.withAppendedId(Download.CONTENT_URI, 3);
case ADDRESSES_CODE:
return ContentUris.withAppendedId(Address.CONTENT_URI, 4);
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
throw new SecurityException("Calling application is not a partner application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 5;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 15;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
throw new SecurityException("Calling application is not a partner application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 10;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 20;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
}
下面是使用伙伴内容供应器的活动示例:
要点(使用内容供应器):
5) 验证目标应用的证书是否已在自己的白名单中注册。
6) 可以发送开放给伙伴应用的信息。
7) 即使数据来自伙伴应用,也要小心并安全地处理收到的结果数据。
PartnerActivity.java
package org.jssec.android.provider.partneruser;
import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class PartnerUserActivity extends Activity {
private static final String AUTHORITY = "org.jssec.android.provider.partnerprovider";
private interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static PkgCertWhitelists sWhitelists = null;
private static void buildWhitelists(Context context) {
boolean isdebug = Utils.isDebuggable(context);
sWhitelists = new PkgCertWhitelists();
sWhitelists.add("org.jssec.android.provider.partnerprovider", isdebug ?
"0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :
"D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA");
}
private static boolean checkPartner(Context context, String pkgname) {
if (sWhitelists == null) buildWhitelists(context);
return sWhitelists.test(context, pkgname);
}
private String providerPkgname(Uri uri) {
String pkgname = null;
ProviderInfo pi = getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
if (pi != null) pkgname = pi.packageName;
return pkgname;
}
public void onQueryClick(View view) {
logLine("[Query]");
if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
logLine(" The target content provider is not served by partner applications.");
return;
}
Cursor cursor = null;
try {
cursor = getContentResolver().query(Address.CONTENT_URI, null, null, null, null);
if (cursor == null) {
logLine(" null cursor");
} else {
boolean moved = cursor.moveToFirst();
while (moved) {
logLine(String.format(" %d, %s", cursor.getInt(0), cursor.getString(1)));
moved = cursor.moveToNext();
}
}
}
finally {
if (cursor != null) cursor.close();
}
}
public void onInsertClick(View view) {
logLine("[Insert]");
if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
logLine(" The target content provider is not served by partner applications.");
return;
}
ContentValues values = new ContentValues();
values.put("city", "Tokyo");
Uri uri = getContentResolver().insert(Address.CONTENT_URI, values);
logLine(" uri:" + uri);
}
public void onUpdateClick(View view) {
logLine("[Update]");
if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
logLine(" The target content provider is not served by partner applications.");
return;
}
ContentValues values = new ContentValues();
values.put("city", "Tokyo");
String where = "_id = ?";
String[] args = { "4" };
int count = getContentResolver().update(Address.CONTENT_URI, values, where, args);
logLine(String.format(" %s records updated", count));
}
public void onDeleteClick(View view) {
logLine("[Delete]");
if (!checkPartner(this, providerPkgname(Address.CONTENT_URI))) {
logLine(" The target content provider is not served by partner applications.");
return;
}
int count = getContentResolver().delete(Address.CONTENT_URI, null, null);
logLine(String.format(" %s records deleted", count));
}
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}
PkgCertWhitelists.java
package org.jssec.android.shared;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
public class PkgCertWhitelists {
private Map<String, String> mWhitelists = new HashMap<String, String>();
public boolean add(String pkgname, String sha256) {
if (pkgname == null) return false;
if (sha256 == null) return false;
sha256 = sha256.replaceAll(" ", "");
if (sha256.length() != 64) return false;
sha256 = sha256.toUpperCase();
if (sha256.replaceAll("[0-9A-F]+", "").length() != 0) return false;
mWhitelists.put(pkgname, sha256);
return true;
}
public boolean test(Context ctx, String pkgname) {
String correctHash = mWhitelists.get(pkgname);
return PkgCert.test(ctx, pkgname, correctHash);
}
}
PkgCert.java
package org.jssec.android.shared;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
public class PkgCert {
public static boolean test(Context ctx, String pkgname, String correctHash) {
if (correctHash == null) return false;
correctHash = correctHash.replaceAll(" ", "");
return correctHash.equals(hash(ctx, pkgname));
}
public static String hash(Context ctx, String pkgname) {
if (pkgname == null) return null;
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
if (pkginfo.signatures.length != 1) return null;
Signature sig = pkginfo.signatures[0];
byte[] cert = sig.toByteArray();
byte[] sha256 = computeSha256(cert);
return byte2hex(sha256);
} catch (NameNotFoundException e) {
return null;
}
}
private static byte[] computeSha256(byte[] data) {
try {
return MessageDigest.getInstance("SHA-256").digest(data);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
private static String byte2hex(byte[] data) {
if (data == null) return null;
final StringBuilder hexadecimal = new StringBuilder();
for (final byte b : data) {
hexadecimal.append(String.format("%02X", b));
}
return hexadecimal.toString();
}
}
4.3.1.4 创建/使用内部内容供应器
内部内容供应器禁止除内部应用以外的应用使用。
下面展示了如何实现内部内容供应器的示例代码。
要点(创建内容供应器):
1) 定义内部签名权限。
2) 需要内部签名权限。
3) 将导出属性显式设置为true。
4) 验证内部签名权限是否由内部应用定义。
5) 验证参数的安全性,即使这是来自内部应用的请求。
6) 由于请求应用是内部的,因此可以返回敏感信息。
7) 导出 APK 时,请使用与请求应用相同的开发者密钥对 APK 进行签名。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.provider.inhouseprovider">
<permission
android:name="org.jssec.android.provider.inhouseprovider.MY_PERMISSION"
android:protectionLevel="signature" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<provider
android:name=".InhouseProvider"
android:authorities="org.jssec.android.provider.inhouseprovider"
android:permission="org.jssec.android.provider.inhouseprovider.MY_PERMISSION"
android:exported="true" />
</application>
</manifest>
InhouseProvider.java
package org.jssec.android.provider.inhouseprovider;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
public class InhouseProvider extends ContentProvider {
public static final String AUTHORITY = "org.jssec.android.provider.inhouseprovider";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.org.jssec.contenttype";
public interface Download {
public static final String PATH = "downloads";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
public interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static final int DOWNLOADS_CODE = 1;
private static final int DOWNLOADS_ID_CODE = 2;
private static final int ADDRESSES_CODE = 3;
private static final int ADDRESSES_ID_CODE = 4;
private static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, Download.PATH, DOWNLOADS_CODE);
sUriMatcher.addURI(AUTHORITY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH, ADDRESSES_CODE);
sUriMatcher.addURI(AUTHORITY, Address.PATH + "/#", ADDRESSES_ID_CODE);
}
private static MatrixCursor sAddressCursor = new MatrixCursor(new String[] { "_id", "city" });
static {
sAddressCursor.addRow(new String[] { "1", "New York" });
sAddressCursor.addRow(new String[] { "2", "London" });
sAddressCursor.addRow(new String[] { "3", "Paris" });
}
private static MatrixCursor sDownloadCursor = new MatrixCursor(new String[] { "_id", "path" });
static {
sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
}
private static final String MY_PERMISSION = "org.jssec.android.provider.inhouseprovider.MY_PERMISSION";
private static String sMyCertHash = null;
private static String myCertHash(Context context) {
if (sMyCertHash == null) {
if (Utils.isDebuggable(context)) {
sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
} else {
sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
}
}
return sMyCertHash;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case ADDRESSES_CODE:
return CONTENT_TYPE;
case DOWNLOADS_ID_CODE:
case ADDRESSES_ID_CODE:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
throw new SecurityException("The in-house signature permission is not declared by in-house application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case DOWNLOADS_ID_CODE:
return sDownloadCursor;
case ADDRESSES_CODE:
case ADDRESSES_ID_CODE:
return sAddressCursor;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
throw new SecurityException("The in-house signature permission is not declared by in-house application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return ContentUris.withAppendedId(Download.CONTENT_URI, 3);
case ADDRESSES_CODE:
return ContentUris.withAppendedId(Address.CONTENT_URI, 4);
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
throw new SecurityException("The in-house signature permission is not declared by in-house application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 5;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 15;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) {
throw new SecurityException("The in-house signature permission is not declared by in-house application.");
}
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 10;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 20;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
}
SigPerm.java
package org.jssec.android.shared;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
public class SigPerm {
public static boolean test(Context ctx, String sigPermName, String correctHash) {
if (correctHash == null) return false;
correctHash = correctHash.replaceAll(" ", "");
return correctHash.equals(hash(ctx, sigPermName));
}
public static String hash(Context ctx, String sigPermName) {
if (sigPermName == null) return null;
try {
PackageManager pm = ctx.getPackageManager();
PermissionInfo pi;
pi = pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
String pkgname = pi.packageName;
if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE) return null;
return PkgCert.hash(ctx, pkgname);
} catch (NameNotFoundException e) {
return null;
}
}
}
PkgCert.java
package org.jssec.android.shared;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
public class PkgCert {
public static boolean test(Context ctx, String pkgname, String correctHash) {
if (correctHash == null) return false;
correctHash = correctHash.replaceAll(" ", "");
return correctHash.equals(hash(ctx, pkgname));
}
public static String hash(Context ctx, String pkgname) {
if (pkgname == null) return null;
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
if (pkginfo.signatures.length != 1) return null;
Signature sig = pkginfo.signatures[0];
byte[] cert = sig.toByteArray();
byte[] sha256 = computeSha256(cert);
return byte2hex(sha256);
} catch (NameNotFoundException e) {
return null;
}
}
private static byte[] computeSha256(byte[] data) {
try {
return MessageDigest.getInstance("SHA-256").digest(data);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
private static String byte2hex(byte[] data) {
if (data == null) return null;
final StringBuilder hexadecimal = new StringBuilder();
for (final byte b : data) {
hexadecimal.append(String.format("%02X", b));
}
return hexadecimal.toString();
}
}
要点 7:导出 APK 时,请使用与请求应用相同的开发者密钥对 APK 进行签名。
![]()
下面是使用内部内容供应器的活动示例。
要点(使用内容个供应器):
8) 声明使用内部签名权限。
9) 验证内部签名权限是否由内部应用定义。
10) 验证目标应用是否使用内部证书签名。
11) 由于目标应用是内部应用,因此可以发送敏感信息。
12) 即使数据来自内部应用,也要小心并安全地处理收到的结果数据。
13) 导出 APK 时,请使用与目标应用相同的开发人员密钥对 APK 进行签名。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.provider.inhouseuser">
<uses-permission
android:name="org.jssec.android.provider.inhouseprovider.MY_PERMISSION" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".InhouseUserActivity"
android:label="@string/app_name"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
InhouseUserActivity.java
package org.jssec.android.provider.inhouseuser;
import org.jssec.android.shared.PkgCert;
import org.jssec.android.shared.SigPerm;
import org.jssec.android.shared.Utils;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class InhouseUserActivity extends Activity {
private static final String AUTHORITY = "org.jssec.android.provider.inhouseprovider";
private interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static final String MY_PERMISSION = "org.jssec.android.provider.inhouseprovider.MY_PERMISSION";
private static String sMyCertHash = null;
private static String myCertHash(Context context) {
if (sMyCertHash == null) {
if (Utils.isDebuggable(context)) {
sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255";
} else {
sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
}
}
return sMyCertHash;
}
private static String providerPkgname(Context context, Uri uri) {
String pkgname = null;
PackageManager pm = context.getPackageManager();
ProviderInfo pi = pm.resolveContentProvider(uri.getAuthority(), 0);
if (pi != null) pkgname = pi.packageName;
return pkgname;
}
public void onQueryClick(View view) {
logLine("[Query]");
if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) {
logLine(" The in-house signature permission is not declared by in-house application.");
return;
}
String pkgname = providerPkgname(this, Address.CONTENT_URI);
if (!PkgCert.test(this, pkgname, myCertHash(this))) {
logLine(" The target content provider is not served by in-house applications.");
return;
}
Cursor cursor = null;
try {
cursor = getContentResolver().query(Address.CONTENT_URI, null, null, null, null);
if (cursor == null) {
logLine(" null cursor");
} else {
boolean moved = cursor.moveToFirst();
while (moved) {
logLine(String.format(" %d, %s", cursor.getInt(0), cursor.getString(1)));
moved = cursor.moveToNext();
}
}
} finally {
if (cursor != null) cursor.close();
}
}
public void onInsertClick(View view) {
logLine("[Insert]");
String correctHash = myCertHash(this);
if (!SigPerm.test(this, MY_PERMISSION, correctHash)) {
logLine(" The in-house signature permission is not declared by in-house application.");
return;
}
String pkgname = providerPkgname(this, Address.CONTENT_URI);
if (!PkgCert.test(this, pkgname, correctHash)) {
logLine(" The target content provider is not served by in-house applications.");
return;
}
ContentValues values = new ContentValues();
values.put("city", "Tokyo");
Uri uri = getContentResolver().insert(Address.CONTENT_URI, values);
logLine(" uri:" + uri);
}
public void onUpdateClick(View view) {
logLine("[Update]");
String correctHash = myCertHash(this);
if (!SigPerm.test(this, MY_PERMISSION, correctHash)) {
logLine(" The in-house signature permission is not declared by in-house application.");
return;
}
String pkgname = providerPkgname(this, Address.CONTENT_URI);
if (!PkgCert.test(this, pkgname, correctHash)) {
logLine(" The target content provider is not served by in-house applications.");
return;
}
ContentValues values = new ContentValues();
values.put("city", "Tokyo");
String where = "_id = ?";
String[] args = { "4" };
int count = getContentResolver().update(Address.CONTENT_URI, values, where, args);
logLine(String.format(" %s records updated", count));
}
public void onDeleteClick(View view) {
logLine("[Delete]");
String correctHash = myCertHash(this);
if (!SigPerm.test(this, MY_PERMISSION, correctHash)) {
logLine(" The target content provider is not served by in-house applications.");
return;
}
e.
String pkgname = providerPkgname(this, Address.CONTENT_URI);
if (!PkgCert.test(this, pkgname, correctHash)) {
logLine(" The target content provider is not served by in-house applications.");
return;
}
use one.
int count = getContentResolver().delete(Address.CONTENT_URI, null, null);
logLine(String.format(" %s records deleted", count));
}
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}
SigPerm.java
package org.jssec.android.shared;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
public class SigPerm {
public static boolean test(Context ctx, String sigPermName, String correctHash) {
if (correctHash == null) return false;
correctHash = correctHash.replaceAll(" ", "");
return correctHash.equals(hash(ctx, sigPermName));
}
public static String hash(Context ctx, String sigPermName) {
if (sigPermName == null) return null;
try {
PackageManager pm = ctx.getPackageManager();
PermissionInfo pi;
pi = pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
String pkgname = pi.packageName;
if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE) return null;
return PkgCert.hash(ctx, pkgname);
} catch (NameNotFoundException e) {
return null;
}
}
}
PkgCert.java
package org.jssec.android.shared;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
public class PkgCert {
public static boolean test(Context ctx, String pkgname, String correctHash) {
if (correctHash == null) return false;
correctHash = correctHash.replaceAll(" ", "");
return correctHash.equals(hash(ctx, pkgname));
}
public static String hash(Context ctx, String pkgname) {
if (pkgname == null) return null;
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
if (pkginfo.signatures.length != 1) return null;
Signature sig = pkginfo.signatures[0];
byte[] cert = sig.toByteArray();
byte[] sha256 = computeSha256(cert);
return byte2hex(sha256);
} catch (NameNotFoundException e) {
return null;
}
}
private static byte[] computeSha256(byte[] data) {
try {
return MessageDigest.getInstance("SHA-256").digest(data);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
private static String byte2hex(byte[] data) {
if (data == null) return null;
final StringBuilder hexadecimal = new StringBuilder();
for (final byte b : data) {
hexadecimal.append(String.format("%02X", b));
}
return hexadecimal.toString();
}
}
要点 13:导出 APK 时,请使用与请求应用相同的开发者密钥对 APK 进行签名。
![]()
4.3.1.5 创建/使用临时内容供应器
临时内容供应器基本上是一个私有内容供应器,但它允许特定的应用访问特定的 URI。通过向目标应用发送一个指定了特殊标志的意图,即可为这些应用提供临时访问权限。内容供应器方的应用可以将访问权限主动授予其他应用,并且还可以将访问权限被动授予索要临时访问权限的应用。
下面展示了实现临时内容供应器的示例代码。
要点(创建内容供应器):
1) 将导出属性显式设置为false。
2) 使用grant-uri-permission指定路径来临时授予访问权。
3) 即使数据来自临时访问应用,也应该消息并安全地处理收到的请求数据。
4) 可以返回公开给临时访问应用的信息。
5) 为意图指定 URI 来授予临时访问权。
6) 为意图指定访问权限来授予临时访问权。
7) 将显式意图发送给应用来授予临时访问权。
8) 将意图返回给请求临时访问权的应用。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.provider.temporaryprovider">
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".TemporaryActiveGrantActivity"
android:label="@string/app_name"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name=".TemporaryProvider"
android:authorities="org.jssec.android.provider.temporaryprovider"
android:exported="false" >
<grant-uri-permission android:path="/addresses" />
</provider>
<activity
android:name=".TemporaryPassiveGrantActivity"
android:label="@string/app_name"
android:exported="true" />
</application>
</manifest>
TemporaryProvider.java
package org.jssec.android.provider.temporaryprovider;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
public class TemporaryProvider extends ContentProvider {
public static final String AUTHORITIY = "org.jssec.android.provider.temporaryprovider";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.org.jssec.contenttype";
public interface Download {
public static final String PATH = "downloads";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIY + "/" + PATH);
}
public interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIY + "/" + PATH);
}
private static final int DOWNLOADS_CODE = 1;
private static final int DOWNLOADS_ID_CODE = 2;
private static final int ADDRESSES_CODE = 3;
private static final int ADDRESSES_ID_CODE = 4;
private static UriMatcher sUriMatcher;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITIY, Download.PATH, DOWNLOADS_CODE);
sUriMatcher.addURI(AUTHORITIY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
sUriMatcher.addURI(AUTHORITIY, Address.PATH, ADDRESSES_CODE);
sUriMatcher.addURI(AUTHORITIY, Address.PATH + "/#", ADDRESSES_ID_CODE);
}
private static MatrixCursor sAddressCursor = new MatrixCursor(new String[] { "_id", "city" });
static {
sAddressCursor.addRow(new String[] { "1", "New York" });
sAddressCursor.addRow(new String[] { "2", "London" });
sAddressCursor.addRow(new String[] { "3", "Paris" });
}
private static MatrixCursor sDownloadCursor = new MatrixCursor(new String[] { "_id", "path" });
static {
sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case ADDRESSES_CODE:
return CONTENT_TYPE;
case DOWNLOADS_ID_CODE:
case ADDRESSES_ID_CODE:
return CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
case DOWNLOADS_ID_CODE:
return sDownloadCursor;
case ADDRESSES_CODE:
case ADDRESSES_ID_CODE:
return sAddressCursor;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
an be returned.
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return ContentUris.withAppendedId(Download.CONTENT_URI, 3);
case ADDRESSES_CODE:
return ContentUris.withAppendedId(Address.CONTENT_URI, 4);
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 5;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 15;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (sUriMatcher.match(uri)) {
case DOWNLOADS_CODE:
return 10;
case DOWNLOADS_ID_CODE:
return 1;
case ADDRESSES_CODE:
return 20;
case ADDRESSES_ID_CODE:
return 1;
default:
throw new IllegalArgumentException("Invalid URI:" + uri);
}
}
}
TemporaryActiveGrantActivity.java
package org.jssec.android.provider.temporaryprovider;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
public class TemporaryActiveGrantActivity extends Activity {
private static final String TARGET_PACKAGE = "org.jssec.android.provider.temporaryuser";
private static final String TARGET_ACTIVITY = "org.jssec.android.provider.temporaryuser.TemporaryUserActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.active_grant);
}
public void onSendClick(View view) {
try {
Intent intent = new Intent();
intent.setData(TemporaryProvider.Address.CONTENT_URI);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, "User Activity not found.", Toast.LENGTH_LONG).show();
}
}
}
TemporaryPassiveGrantActivity.java
package org.jssec.android.provider.temporaryprovider;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
public class TemporaryPassiveGrantActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.passive_grant);
}
public void onGrantClick(View view) {
Intent intent = new Intent();
intent.setData(TemporaryProvider.Address.CONTENT_URI);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(Activity.RESULT_OK, intent);
finish();
}
public void onCloseClick(View view) {
finish();
}
}
下面是临时内容供应器的示例。
要点(使用内容供应器):
9) 不要发送敏感信息。
10) 收到结果时,小心并安全地处理结果数据。
TemporaryUserActivity.java
package org.jssec.android.provider.temporaryuser;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class TemporaryUserActivity extends Activity {
private static final String TARGET_PACKAGE = "org.jssec.android.provider.temporaryprovider";
private static final String TARGET_ACTIVITY = "org.jssec.android.provider.temporaryprovider.TemporaryPassiveGrantActivity";
private static final String AUTHORITY = "org.jssec.android.provider.temporaryprovider";
private interface Address {
public static final String PATH = "addresses";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
private static final int REQUEST_CODE = 1;
public void onQueryClick(View view) {
logLine("[Query]");
Cursor cursor = null;
try {
if (!providerExists(Address.CONTENT_URI)) {
logLine(" Content Provider doesn't exist.");
return;
}
cursor = getContentResolver().query(Address.CONTENT_URI, null, null, null, null);
if (cursor == null) {
logLine(" null cursor");
} else {
boolean moved = cursor.moveToFirst();
while (moved) {
logLine(String.format(" %d, %s", cursor.getInt(0), cursor.getString(1)));
moved = cursor.moveToNext();
}
}
} catch (SecurityException ex) {
logLine(" Exception:" + ex.getMessage());
}
finally {
if (cursor != null) cursor.close();
}
}
public void onGrantRequestClick(View view) {
Intent intent = new Intent();
intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
try {
startActivityForResult(intent, REQUEST_CODE);
} catch (ActivityNotFoundException e) {
logLine("Content Provider's Activity not found.");
}
}
private boolean providerExists(Uri uri) {
ProviderInfo pi = getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
return (pi != null);
}
private TextView mLogView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLogView = (TextView)findViewById(R.id.logview);
}
private void logLine(String line) {
mLogView.append(line);
mLogView.append("¥n");
}
}