手机中的联系人信息算得上是我们手机中的一大隐私了,我们当然不希望手机中的联系人信息被窥探被泄露出去,让人不能如愿的是,目前好多的App都会要求阅读手机联系人信息权限,当然那些大厂我们就不说了,比如QQ微信,还有支付宝,毕竟他们已经是我们生活离不开的应用了,只能期望他们大厂会保护我们的隐私,可恶的是一些小厂的App也厚无颜耻的要求阅读手机联系人权限,这就很让人无语了!
所以今天给大家讲讲该怎么禁止应用访问手机联系人信息,保护我们的隐私不容侵犯~
首先老样子,我们还是来分析一下获取手机联系人的途径。平常我们大多通过内容提供器来获取联系人信息,例如博主下面的代码:
Cursor cursor=ContactModel.getContactModel().getActivity().getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if (cursor!=null){
while (cursor.moveToNext()){
Contact contact=new Contact(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)),cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
ContactModel.getContactModel().getContacts().add(contact);
}
}
代码中,ContactModel类和Contact类是我自定义的类,其中获取到的信息,联系人姓名和手机号码都存在了Contact实体类中,在ContactModel中有一个Contact类的List,负责管理获取到的信息。
上面并不是要讲的重点,我们重点来看这句代码:
getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
在这里我们获取Cursor的实例,正是通过解析这个实例来获得我们想要的信息。
下面我们就分析一下思路:既然是解析的Cursor的实例,那么如果我们拦截到相关的函数,把Cursor的实例返回空值,那么不就是解析不成了么,这样也就禁止了应用偷偷看我们的隐私!
思路有了说干就干,我们可以看到返回Cursor的实例的方法是ContentResolver类的query()方法,我们过去看一下它的源码:
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
return query(uri, projection, selection, selectionArgs, sortOrder, null);
}
我们很快就发现,这不是它的实现方法,因为在这里它只是单纯的返回了另外一个query方法!
那好,我们接着向下找:
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal) {
Bundle queryArgs = createSqlQueryBundle(selection, selectionArgs, sortOrder);
return query(uri, projection, queryArgs, cancellationSignal);
}
我们可以从代码看出,这个query()方法和上一个相比,多出一条创建Bundle的语句,然后最后返回的又是一个query()方法!
多出的那条创建语句我们暂时不能考虑,这里不是我们的重点,既然这个还不是最终实现方法,那么下一个query()方法估计应该是真正的方法了,我们这就去看一下:
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable Bundle queryArgs,
@Nullable CancellationSignal cancellationSignal) {
Preconditions.checkNotNull(uri, "uri");
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
long startTime = SystemClock.uptimeMillis();
ICancellationSignal remoteCancellationSignal = null;
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
remoteCancellationSignal = unstableProvider.createCancellationSignal();
cancellationSignal.setRemote(remoteCancellationSignal);
}
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
// This is exciting!!1!!1!!!!1
unstableProviderDied(unstableProvider);
stableProvider = acquireProvider(uri);
if (stableProvider == null) {
return null;
}
qCursor = stableProvider.query(
mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
}
// Force query execution. Might fail and throw a runtime exception here.
qCursor.getCount();
long durationMillis = SystemClock.uptimeMillis() - startTime;
maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);
// Wrap the cursor object into CursorWrapperInner object.
final IContentProvider provider = (stableProvider != null) ? stableProvider
: acquireProvider(uri);
final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
stableProvider = null;
qCursor = null;
return wrapper;
} catch (RemoteException e) {
// Arbitrary and not worth documenting, as Activity
// Manager will kill this process shortly anyway.
return null;
} finally {
if (qCursor != null) {
qCursor.close();
}
if (cancellationSignal != null) {
cancellationSignal.setRemote(null);
}
if (unstableProvider != null) {
releaseUnstableProvider(unstableProvider);
}
if (stableProvider != null) {
releaseProvider(stableProvider);
}
}
}
从代码量上来看,我们终于找到了最终的query()实现方法,赶快去看一下:我们找重点Return返回看起;
第一个return:
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
这段代码位于方法的开始处,我们可以得知query()方法首先通过acquireUnstableProvider()方法获取了一个IContentProvider 类的实例,然后判断是否获取成功,如果获取失败,就返回null空值,也就是返回一个空的Cursor类对象;获取成功就接着进行下面的操作。
第二个return:
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
// This is exciting!!1!!1!!!!1
unstableProviderDied(unstableProvider);
stableProvider = acquireProvider(uri);
if (stableProvider == null) {
return null;
}
qCursor = stableProvider.query(
mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
这个地方的return返回也是一个null。代码可以看出,当执行
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
语句报DeadObjectException错误的时候,将会进行一些清理操作,通过acquireProvider()方法获取一个IContentProvider类的对象,然后判断是否获取成功,获取失败同样会返回一个null值。可想而知,在这里获取IContentProvider类的对象大部分应该为null,毕竟这里进行的是错误处理操作,返回null值为正常操作。
第三处return:
if (qCursor == null) {
return null;
}
还是返回null!代码很简单,判断了一个对象qCursor是否为空,为空则返回null值。这个qCursor对象在哪里声明的呢,我们还要去看一下:
Cursor qCursor = null;
try {
long startTime = SystemClock.uptimeMillis();
ICancellationSignal remoteCancellationSignal = null;
if (cancellationSignal != null) {
cancellationSignal.throwIfCanceled();
remoteCancellationSignal = unstableProvider.createCancellationSignal();
cancellationSignal.setRemote(remoteCancellationSignal);
}
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
// This is exciting!!1!!1!!!!1
unstableProviderDied(unstableProvider);
stableProvider = acquireProvider(uri);
if (stableProvider == null) {
return null;
}
qCursor = stableProvider.query(
mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
}
原来是CurSor类的对象!看一下对它的处理:其中一处try关键字段里,我们提到过了,执行了IContentProvider类的query()方法去赋值,如果出现了DeadObjectException 错误,那么将会重新申请一个IContentProvider类的实例继续执行query()方法来为qCursor赋值。然后如果这两次都赋值失败,那么就会返回null值。
看到这里,我们也明白了,其实真正被调用的是IContentProvider类的query()方法。
接着看第四处return:
qCursor.getCount();
long durationMillis = SystemClock.uptimeMillis() - startTime;
maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);
// Wrap the cursor object into CursorWrapperInner object.
final IContentProvider provider = (stableProvider != null) ? stableProvider
: acquireProvider(uri);
final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
stableProvider = null;
qCursor = null;
return wrapper;
好了,我们终于看到不是返回一个null值的了!那么被正确返回的就应该是这个wrapper对象了,wrapper是CursorWrapperInner 类的对象,通过调用了CursorWrapperInner 类的构建函数来创建,我们还发现,上面的Cursor对象qCursor,在这里被当作了参数传了进入。这里你可能会说,这里返回的类型是Cursor类,怎么正确返回的是CursorWrapperInner 类呢?
我们可以再细致的看一下,发现CursorWrapperInner 类与Cursor类存在着继承关系。具体的继承关系为:
CursorWrapperInner 继承CrossProcessCursorWrapper继承CursorWrapper继承Cursor。继承了三代。
最后一处return也是返回为null值,位于最后的catch代码块中,没有别的多于代码,这里就不会细致的去分析他了。
好了,分析过query()方法的源码之后,我们下面就开始进行拦截它:毕竟那些偷窥的应用们大多数走了query()方法。我们的目标是,拦截到query()方法后,设置返回值为null,这样应用拿不到Cursor的实例,自然就无法解析我们手机中的联系人信息了~
既然锁定了目标,那就开始我们的拦截:目标类是ContentResolver类,目标方法是query()方法。
上面分析了query()方法后,我们发现这个方法并不是很好拦截,因为它首先参数复杂,可以看一下它的参数:
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable Bundle queryArgs,
@Nullable CancellationSignal cancellationSignal)
首先是Uri类型,下面是一个String数组,然后是Bundle类型,最后是CancellationSignal 类型,参数多,参数类型复杂,拦截难度大。还有一个因素,那就是在ContentResolver类中,有三个query()同名方法,这又是一个难度挑战。
我们可以去尝试拦截一下,代码如下:
XposedHelpers.findAndHookMethod(ContentResolver.class, "query", Uri.class,String [].class,Bundle.class,CancellationSignal.class,new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("进入函数");
param.setResult(null);
}
});
运行代码,发现拦截失败,报错为找不到该方法。
那怎么办?既然拦截query()方法困难重重,也就意味着拦截query()方法是不明智的,那么我们该如何正确的去拦截呢?
看来只能取巧进行,来一个曲线救国!还记得刚才我给你分析的query()方法的源码吗?我们发现会有四种情况导致query()方法返回为空,这就给我们带来了一个契机~
我们随便挑一个,就拿第一个return null来讲吧!毕竟query()方法首先执行的就是它,我们回顾一下它的条件:
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
我们发现,这里如果获取IContentProvider 类对象失败就会返回null值,那如果我们拦截这个获取方法acquireUnstableProvider()方法,设置它的返回值为null值,那么也就意味着query()方法也会返回null值!
说干就干,我们去看一下acquireUnstableProvider()方法的源码:
public final IContentProvider acquireUnstableProvider(Uri uri) {
if (!SCHEME_CONTENT.equals(uri.getScheme())) {
return null;
}
String auth = uri.getAuthority();
if (auth != null) {
return acquireUnstableProvider(mContext, uri.getAuthority());
}
return null;
}
看得出这个方法很适合被拦截,首先它的参数构成很简单,就只有一个Uri类,再者它在ContentResolver类中不存在同名方法。无疑,acquireUnstableProvider()方法才是最佳的拦截选择!
现在修改我们的代码:目标类还是ContentResolver类,目标方法是acquireUnstableProvider():
XposedHelpers.findAndHookMethod(ContentResolver.class, "acquireUnstableProvider", Uri.class,new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("进入函数");
if(param.args[0]== ContactsContract.CommonDataKinds.Phone.CONTENT_URI){
param.setResult(null);
}
}
});
在这里我们可以看到做出了一个if判断处理,我们判断的条件就是请求的参数URI是不是Phone.CONTENT_URI。这里的CONTENT_URI就是Phone类已经封装好的请求联系人的URI,你当然也可以选择自己手写一条URI:content://com.android.contacts/raw_contacts,不过我要告诉你在这里,Phone.CONTENT_URI的值等同于content://com.android.contacts/raw_contacts。不信我们可以去看一下源码:
//这个是Phone.CONTENT_URI
public static final Uri CONTENT_URI = Uri.withAppendedPath(Data.CONTENT_URI,
"phones");
//这个是Data.CONTENT_URI
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data");
public static final String AUTHORITY = "com.android.contacts";
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
从上面的源码我们就可以看出来Phone.CONTENT_URI的具体值确实是和自己手写等同的,所以我们在获取query()方法中Uri参数填入Phone.CONTENT_URI没有任何问题。
那么接下来就说一下关于拦截到函数后做出判断Uri的问题。首先我们要知道,在一个应用中,可能会有多处地方需要使用系统的内容提供器,以便获取系统中的一些数据,比如这里我想要获取系统的音乐信息,也是要通过query()方法来获取Cursor类实例然后进行解析获取数据,只不过传入的Uri参数是不一样的。所以如果拦截到方法后不去判断Uri的值,那么导致的后果就是无论传入的什么Uri都无法获取到Cursor实例,无法从内容提供器中获取任何一种数据!所以这里我们就需要判断Uri的值,如果是获取手机联系人信息的Uri,我们才会给结果返回为null值,让它拿不到数据。
运行我们修改后的代码,执行成功!我们成功的令query()方法返回了null值,以后再也不会担心那些令人讨厌的应用偷窥我们的隐私了,因为他们根本就偷窥不到!拦截到方法后就把它的返回值设为null,也就是把IContentProvider 类对象设为null,程序在下一步判断是否获取成功的时候就会执行return null操作,返回Cursor对象也为空。
最后说一下,开发程序永远不能死劲,思维不要固化,代码是灵活多变的,并不存在一个固定的格式和写法。在平常的开发中,大家要多多的开动脑筋和思维,懂得变通,敢于尝试,这样才能使自己开发出越来越优秀的代码。
本文到此结束,需要引用本文的地方,请标明出处,谢谢!