转:http://vbill.github.io/2015/02/13/xposed-appsettings/
本文分析一个权限管理类Xposed模块的源代码,主要分析权限管理功能实现的原理。完全按照本人看代码的顺序写成。写此文主要不是为了分析代码,而是总结这种分析代码的思路。所以,懒得看过程可以直接跳到代码分析。
准备工作
下载好源代码,还要在手机上把这个程序安装好,这样能直观感受它的功能。
大致思路
我们最关键的任务是找到权限控制的核心代码并弄明白它的功能。但是这么多的文件无从下手。我的想法是结合程序的实际操作,然后翻出来相应的代码。
直奔主题
先看AndroidManifest.xml,因为我们要找程序启动界面。除了xposed的meta data,有个定义launcher activity的代码:
<activity
android:name=".XposedModActivity"
android:label="@string/app_name"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
就是说类de.robv.android.xposed.mods.appsettings.XposedModActivity
包含启动界面的代码。
在手机上打开程序,会看到启动界面主要是一个ListView,每项里面放着app的名称和包名。点击其中某项会跳到新的Activity里。那么目标明确了,找这个跳转代码。
我用startAcitivity为关键词找到了:
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Open settings activity when clicking on an application
String pkgName = ((TextView) view.findViewById(R.id.app_package)).getText().toString();
Intent i = new Intent(getApplicationContext(), ApplicationSettings.class);
i.putExtra("package", pkgName);
startActivityForResult(i, position);
}
});
可见,ApplicationSettings这个类包含了新的Activity的代码。
新界面只有一个switch,打开switch后所有的选项都出来了。我手机的左下角出现了一个叫”权限管理“的按钮。点开以后是一个对话框,里面的ListView罗列了所有的应用权限让我们修改。单击List里的某项,权限就被禁用了,同时权限的字体由白变紫。
所以我先找这个按钮的代码:
btnPermissions.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// set up permissions editor
try {
final PermissionSettings permsDlg = new PermissionSettings(ApplicationSettings.this, pkgName, allowRevoking, disabledPermissions);
permsDlg.setOnOkListener(new PermissionSettings.OnDismissListener() {
@Override
public void onDismiss(PermissionSettings obj) {
allowRevoking = permsDlg.getRevokeActive();
disabledPermissions.clear();
disabledPermissions.addAll(permsDlg.getDisabledPermissions());
}
});
permsDlg.display();
} catch (NameNotFoundException e) {
}
}
});
看来PermissionSettings
就是权限管理界面的类。在这个类中唯一被我发现的和ListView有关的代码:
// Load the list of permissions for the package and present them
loadPermissionsList(pkgName);
final PermissionsListAdapter appListAdapter = new PermissionsListAdapter(owner, permsList, disabledPerms, true);
appListAdapter.setCanEdit(revokeActive);
((ListView) dialog.findViewById(R.id.lstPermissions)).setAdapter(appListAdapter);
所以说处理单击ListView项并改变程序权限的代码应该在别的地方。
只能是在Adapter的代码里了:
if (allowEdits) {
row.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!canEdit) {
return;
}
TextView tv = (TextView) v.findViewById(R.id.perm_name);
if ((tv.getPaintFlags() & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
disabledPerms.remove(tv.getTag());
tv.setPaintFlags(tv.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
tv.setTextColor(Color.WHITE);
} else {
disabledPerms.add((String) tv.getTag());
tv.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
tv.setTextColor(Color.MAGENTA);
}
}
});
}
在这里disabledPerms
是一个Set<String>
类型的对象。顾名思义,这里面方的可能是被禁用的权限。上面代码并没有直接处理权限的部分。结合Diaglog界面有“确定”按钮,推测最后程序先将权限添加到Set中,然后统一禁止权限。
那么disabledPerms
怎么传递出去的呢?搜索整个Adapter的代码,看到构造函数里有一句:this.disabledPerms = disabledPerms;
。再回头看该才找到的PermissionSettings
类里和List唯一有关的代码里有:
final PermissionsListAdapter appListAdapter = new PermissionsListAdapter(owner, permsList, disabledPerms, true);
所以disabledPerms
就是我们要找的。下一步可以找PermissionSettings
里的这个disabledPerms
里的结果怎么返回回去的。搜索这个类里的代码,找到了get方法:
/**
* Get the list of permissions in the disabled state
*/
public Set<String> getDisabledPermissions() {
return new HashSet<String>(disabledPerms);
}
我们之前找到的ApplicationSettings
里面的btnPermissions.setOnClickListener
里有这么一行:disabledPermissions.addAll(permsDlg.getDisabledPermissions());
。
另外disabledPermissions
只有声明没有定义。在onCreate()
方法里它才被赋值:
// Setting for permissions revoking
allowRevoking = prefs.getBoolean(pkgName + Common.PREF_REVOKEPERMS, false);
disabledPermissions = prefs.getStringSet(pkgName + Common.PREF_REVOKELIST, new HashSet<String>());
同时在private Map<String, Object> getSettings()
这个方法结尾处有这么两行:
if (disabledPermissions.size() > 0)
settings.put(pkgName + Common.PREF_REVOKELIST, new HashSet<String>(disabledPermissions));
方法的返回值就是settings。如果再看看整个方法的代码,可知这个应用的所有被修改内容全放到这个settings
里了。
在onOptionsItemSelected
里出现了getSettings()
的调用,同时还有很多sharedPreference的操作。在手机上,我们修改应用权限,然后单击右上角的保存按钮,弹出提示对话框,询问是否结束进程以便下次启动时采用新设置。根据这点找到代码:
prefsEditor.commit();
// Update saved settings to detect modifications later
initialSettings = newSettings;
// Check if in addition to saving the settings, the app should also be killed
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.settings_apply_title);
builder.setMessage(R.string.settings_apply_detail);
builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Send the broadcast requesting to kill the app
Intent applyIntent = new Intent(Common.MY_PACKAGE_NAME + ".UPDATE_PERMISSIONS");
applyIntent.putExtra("action", Common.ACTION_PERMISSIONS);
applyIntent.putExtra("Package", pkgName);
applyIntent.putExtra("Kill", true);
sendBroadcast(applyIntent, Common.MY_PACKAGE_NAME + ".BROADCAST_PERMISSION");
dialog.dismiss();
}
});
整个工程只有一个PackagePermissions类是BroadcastReceiver类。打开后发现这个类有大量的Xposed的hook函数。那么问题来了:
- 广播接受器什么时候开始工作的?
- 哪些是hook权限的操作?
- hook是如何在开机时就开始了(否则没办法监控权限)
在BroadcastReceiver里叫initHooks()的静态方法里找到:
final Class<?> clsPMS = findClass("com.android.server.pm.PackageManagerService", XposedMod.class.getClassLoader());
// Listen for broadcasts from the Settings part of the mod, so it's applied immediately
findAndHookMethod(clsPMS, "systemReady", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Context mContext = (Context) getObjectField(param.thisObject, "mContext");
mContext.registerReceiver(new PackagePermissions(param.thisObject),
new IntentFilter(Common.MY_PACKAGE_NAME + ".UPDATE_PERMISSIONS"),
Common.MY_PACKAGE_NAME + ".BROADCAST_PERMISSION",
null);
}
});
// if the user has disabled certain permissions for an app, do as if the hadn't requested them
findAndHookMethod(clsPMS, "grantPermissionsLPw", "android.content.pm.PackageParser$Package", boolean.class,
new XC_MethodHook() {
@SuppressWarnings("unchecked")
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
String pkgName = (String) getObjectField(param.args[0], "packageName");
if (!XposedMod.isActive(pkgName) || !XposedMod.prefs.getBoolean(pkgName + Common.PREF_REVOKEPERMS, false))
return;
Set<String> disabledPermissions = XposedMod.prefs.getStringSet(pkgName + Common.PREF_REVOKELIST, null);
if (disabledPermissions == null || disabledPermissions.isEmpty())
return;
ArrayList<String> origRequestedPermissions = (ArrayList<String>) getObjectField(param.args[0], "requestedPermissions");
param.setObjectExtra("orig_requested_permissions", origRequestedPermissions);
ArrayList<String> newRequestedPermissions = new ArrayList<String>(origRequestedPermissions.size());
for (String perm: origRequestedPermissions) {
if (!disabledPermissions.contains(perm))
newRequestedPermissions.add(perm);
else
// you requested those internet permissions? I didn't read that, sorry
Log.w(Common.TAG, "Not granting permission " + perm
+ " to package " + pkgName
+ " because you think it should not have it");
}
setObjectField(param.args[0], "requestedPermissions", newRequestedPermissions);
}
@SuppressWarnings("unchecked")
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// restore requested permissions if they were modified
ArrayList<String> origRequestedPermissions = (ArrayList<String>) param.getObjectExtra("orig_requested_permissions");
if (origRequestedPermissions != null)
setObjectField(param.args[0], "requestedPermissions", origRequestedPermissions);
}
分析代码
BroadcastReceiver里的注释说:
/* Hook to the PackageManager service in order to
* - Listen for broadcasts to apply new settings and restart the app
* - Intercept the permission granting function to remove disabled permissions
*/
也就是说监听器hook到了Android系统的包管理器类com.android.server.pm.PackageManagerService
,并且hook了里面的方法。所以上面提到的哪些是hook权限的操作基本上解决了,代码之后详细分析。那么广播什么时候开始接收的?在IntelliJ里搜索BroadcastReceiver被用到的地方,发现:
- XposedMod类中
initZygote
方法里出现PackagePermissions.initHooks();
- 刚才贴出的大段BroadcastReceiver里出现
mContext.registerReceiver
里面有它的构造函数。
initZygote
是Xposed框架IXposedHookZygoteInit
接口中要自己实现的方法。接口的源代码为:
/**
* Hook the initialization of Zygote (the central part of the "Android OS")
*/
public interface IXposedHookZygoteInit extends IXposedMod {
/**
* Called very early during startup of Zygote
* @throws Throwable everything is caught, but will prevent further initialization of the module
*/
public void initZygote(StartupParam startupParam) throws Throwable;
public static class StartupParam {
public String modulePath;
}
}
这就意味着每当启动一个进程,都会执行initZygote
里监听器的initHooks()
方法来给包管理器挂钩。权限监听的钩子应该挂到com.android.server.pm.PackageManagerService
这个类的名为的grantPermissionsLPw
方法上。程序用了Xposed框架提供的findAndHookMethod
方法。通过这个方法接收的参数,我们得知被hook的grantPermissionsLPw
方法接收两个参数,分别是Package类型(android.content.pm.PackageParser的内部类),和boolean。initHooks()
方法还顺带注册监听器来接受来自appSettings这个app自己发出的广播。再进一步查看工程代码和安卓源代码,就知道,appSettings的权限拦截原理是这样的:
原来的权限授权以前先:从之前appsSettings存储的sharedPreferences里取得对应应用的权限列表,放到Set<String> disabledPermissions
里。并用ArrayList<String> origRequestedPermissions
存放应用索要的权限,并利用Xposed自带的方法存储一份这个权限列表到param
对象里。然后通过for each循环,对比两个ArrayList,生成第三张表newRequestedPermissions
,并用setObjectField
方法替换掉了原来Package对象的requestedPermissions
对象。
之后,安卓系统会按照被我们“调包”的权限清单执行程序。
被hook过的方法执行以后:从param对象取出我们刚刚保存的原始的权限列表,然后再次用setObjectField
把这个原始列表复原回去。
于是第二个问题,哪些是hook操作,怎么hook的问题就解决了。
最后,我们打开工程目录下assets/xposed_init
文件,看到de.robv.android.xposed.mods.appsettings.XposedMod
。所以,Xposed框架开始执行的就是这个类里的代码。它实现了IXposedHookZygoteInit
与 IXposedHookLoadPackage
接口。所以能:1.在zygote启动时执行,从而管理权限。2.在应用app的包加载前执行hook操作,替换应用资源(这是appSettings另一个功能,但本文不分析)。
所以,为何启动时就能hook的问题也解决了。