我有一个应用程序,当由ContentObserver
通知更改到ContentProvider
时,试图在后台线程上查询提供者。这会导致抛出一个SecurityException
:
一个应用程序创建的线程如何与应用程序的ContentProvider的不同UID结束?
通过在android.content.ContentProvider
中放置异常断点,我看到UserHandle.isSameApp(uid, mMyUid)
是false
, UserHandle.isSameUser(uid, mMyUid)
是true
。我还看到提供者的UID是10087。
uid值1000属于Android系统。Android的许多特性都涉及到将请求代理到系统线程进行处理。如果在此期间抛出异常,错误将包括系统的uid,而不是原始请求者。
其他点:
UserHandle.isSameApp(uid, mMyUid) is false
UserHandle.isSameUser(uid, mMyUid) is true
这些最容易通过查看源代码来解释。在支持多用户的Android设备上,每个用户由一系列uid定义。isSameApp
为false,因为id的模数不匹配:
public static final boolean isSameApp(int uid1, int uid2) {
return getAppId(uid1) == getAppId(uid2);
}
public static final int getAppId(int uid) {
return uid % PER_USER_RANGE;
}
同样,这两个id属于同一个用户,因为它们位于同一个范围内:
public static final boolean isSameUser(int uid1, int uid2) {
return getUserId(uid1) == getUserId(uid2);
}
public static final int getUserId(int uid) {
if (MU_ENABLED) {
return uid / PER_USER_RANGE;
} else {
return 0;
}
}
请注意,这个逻辑是有缺陷的,因为它意味着所有的Android系统id (<10000)将被认为"属于"第一个用户。
还要注意,如果第二个用户安装了超过1000个应用程序(!),那么应用程序就有可能被误认为是系统应用程序(两个uid % PER_USER_RANGE
都会返回1000)。不过这并不重要,因为强大的沙箱会防止任何太糟糕的发生。
在系统回调(LeScanCallback
)中尝试与我的ContentProvider交互时,我遇到了同样的问题。问题是回调线程由Android系统拥有,而不是由我的应用程序拥有,即使代码在我的应用程序中。
在尝试与我的ContentProvider交互之前,将工作从回调传递给我的一个应用线程,成功地解决了这个问题。
为了减少线程创建和回收的模板(需要频繁回调以减少开销),我在我的委托方法上使用了AndroidAnnotation的@Background
注释(但今天将使用Kotlin Coroutines)。
如果Thread
是由具有该提供者的应用程序的任何组件启动的,那么您可以在没有SecurityException
的情况下访问ContentProvider
。
我在我的应用程序中使用ContentProvider
只是作为一个额外的抽象层,我还没有将内容暴露给其他应用程序。我在后台线程访问ContentProvider
(不是AsyncTask
,而是一个简单的java.lang.Thread
)。我没有得到任何SecurityException
。以下是我的应用程序的代码:
AndroidManifest.xml
<provider
android:authorities="com.sample.provider"
android:name="com.sample.MyProvider"
android:exported="false" />
MainActivity
public void performContinue(Bundle extras){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
String AUTHORITY = "com.sample.provider";
Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
Uri currentUri = BASE_URI.buildUpon().appendPath("SAMPLE_COUNT").build();
final Cursor query = InputActivity.this.getContentResolver().query(currentUri, null, null, null, null);
if (query != null) {
final int count = query.getCount();
Log.d("DEBUG","CONTENT = " + count);
}else{
Log.d("DEBUG","CONTENT = CURSOR NULL");
}
}
});
thread.setName("THREAD_1");
thread.start();
}
我似乎没有得到任何SecurityException
。理想情况下,我们需要使用AsyncQueryHandler
来访问ContentProvider
,因为这允许你在后台线程中完成所有的抓取过程,并将使用UI线程在UI中发布结果。但是在看到这篇文章之后,我只是想看看我是否可以使用Thread
,并检查我是否仍然可以毫无例外地访问它。