最近,项目要做Android TV的运行时权限管理模块,网上搜索了很久,搜出来的资料都是关于请求运行时权限和跳转到设置的权限管理界面的,没搜到打开、关闭权限开关时,要调用什么API可以实现打开、关闭某个应用的某个权限。所以,就想看看原生Setting中是如何实现的。
先发个看Google官方源码的地址(需要科学上网):https://android.googlesource.com/,
其中看TV Settings源码的路径在 https://android.googlesource.com/platform/packages/apps/TvSettings/,如下图所示:
因为我想看的是Android 6.0的源码,所以我去Branch找了一个android-6.0.0.0 r1的分支,如图所示。
为了快速找到自己需要的权限管理部分的代码,我首先打开了Settings的权限管理页面,如下图所示。
然后使用
adb shell dumpsys activity top
命令找到了当前栈顶Activity,如下图所示,然后再去对比源码中该部分代码的位置。图中箭头指向的就是栈顶Activity,也就是
com.android.tv.settings/.device.apps.AppManagementActivity,栈顶Activity找到后,就可以去源码中找一找相关代码了,
果然,在源码中找到了这个类,那接下来就是要看看源码中都做了些什么操作了。
摘抄了下面一部分代码,发现这部分创建布局的代码和图三的布局类似,那我猜测应该也能找到对应的按键的点击事件,虽然本来就是冲着这个点击事件来的,那好吧,再看看每一部分的点击事件在哪。
@Override
public Layout createLayout() {
final Resources res = getResources();
mAppIconDrawableGetter.refreshFromAppInfo(mAppInfo);
final Layout.Header header = new Layout.Header.Builder(res)
.title(mAppInfo.getName())
.detailedDescription(getString(R.string.device_apps_app_management_version,
mAppInfo.getVersion()))
.icon(mAppIconDrawableGetter)
.build();
// Open
if (mOpenManager.canOpen()) {
header.add(new Layout.Action.Builder(res, mOpenManager.getLaunchIntent())
.title(R.string.device_apps_app_management_open)
.build());
}
// Force Stop
if (mForceStopManager.canForceStop()) {
header.add(new Layout.Header.Builder(res)
.title(R.string.device_apps_app_management_force_stop)
.detailedDescription(R.string.device_apps_app_management_force_stop_desc)
.build()
.add(new Layout.Action.Builder(res, ACTION_FORCE_STOP)
.title(android.R.string.ok)
.build())
.add(new Layout.Action.Builder(res, Layout.Action.ACTION_BACK)
.title(android.R.string.cancel)
.defaultSelection()
.build()));
}
// Uninstall/Disable/Enable
header.add(mUninstallLayoutGetter);
// Storage used
header.add(new Layout.Action.Builder(res, ACTION_STORAGE_USED)
.title(R.string.device_apps_app_management_storage_used)
.description(mStorageDescriptionGetter)
.build());
// Clear data
header.add(new Layout.Header.Builder(res)
.title(R.string.device_apps_app_management_clear_data)
.detailedDescription(R.string.device_apps_app_management_clear_data_desc)
.description(mDataDescriptionGetter)
.build()
.add(new Layout.Action.Builder(res, ACTION_CLEAR_DATA)
.title(android.R.string.ok)
.build())
.add(new Layout.Action.Builder(res, Layout.Action.ACTION_BACK)
.title(android.R.string.cancel)
.defaultSelection()
.build()));
// Clear cache
header.add(new Layout.Header.Builder(res)
.title(R.string.device_apps_app_management_clear_cache)
.description(mCacheDescriptionGetter)
.build()
.add(new Layout.Action.Builder(res, ACTION_CLEAR_CACHE)
.title(android.R.string.ok)
.build())
.add(new Layout.Action.Builder(res, Layout.Action.ACTION_BACK)
.title(android.R.string.cancel)
.defaultSelection()
.build()));
// Clear defaults
header.add(new Layout.Header.Builder(res)
.title(R.string.device_apps_app_management_clear_default)
.description(mDefaultsDescriptionGetter)
.build()
.add(new Layout.Action.Builder(res, ACTION_CLEAR_DEFAULTS)
.title(android.R.string.ok)
.build())
.add(new Layout.Action.Builder(res, Layout.Action.ACTION_BACK)
.title(android.R.string.cancel)
.defaultSelection()
.build()));
// Notifications
header.add(new Layout.Header.Builder(res)
.title(R.string.device_apps_app_management_notifications)
.build()
.setSelectionGroup(
new Layout.SelectionGroup.Builder(2)
.add(getString(R.string.settings_on), null,
ACTION_NOTIFICATIONS_ON)
.add(getString(R.string.settings_off), null,
ACTION_NOTIFICATIONS_OFF)
.select((mNotificationSetter.areNotificationsOn())
? ACTION_NOTIFICATIONS_ON : ACTION_NOTIFICATIONS_OFF)
.build()));
// Permissions
header.add(new Layout.Action.Builder(res, ACTION_PERMISSIONS)
.title(R.string.device_apps_app_management_permissions)
.build());
return new Layout().breadcrumb(getString(R.string.device_apps)).add(header);
}
很快就找到了下面这部分代码,毕竟这么多case很显眼,猜一下也会觉得这些点击事件和创建的布局应该是一 一对应的,果然啊,看点击事件的命名,很像啊。接着就看看 ACTION_PERMISSIONS
这个case了,觉得应该不会错,毕竟人家内部的方法是 startManagePermissionsActivity();
,光看命名都觉得差不多。
@Override
public void onActionClicked(Layout.Action action) {
switch (action.getId()) {
case ACTION_FORCE_STOP:
onForceStopOk();
break;
case ACTION_STORAGE_USED:
startDialogFragment(MoveAppStepFragment.newInstance(mPackageName,
mAppInfo.getName()));
break;
case ACTION_CLEAR_DATA:
onClearDataOk();
break;
case ACTION_CLEAR_CACHE:
onClearCacheOk();
break;
case ACTION_CLEAR_DEFAULTS:
onClearDefaultOk();
break;
case ACTION_NOTIFICATIONS_ON:
onNotificationsOn();
break;
case ACTION_NOTIFICATIONS_OFF:
onNotificationsOff();
break;
case ACTION_PERMISSIONS:
startManagePermissionsActivity();
break;
case ACTION_UNINSTALL:
onUninstallOk();
break;
case ACTION_DISABLE:
onDisableOk();
break;
case ACTION_ENABLE:
onEnableOk();
break;
case ACTION_UNINSTALL_UPDATES:
onUninstallUpdatesOk();
case Layout.Action.ACTION_INTENT:
final Intent intent = action.getIntent();
if (intent != null) {
try {
startActivity(intent);
} catch (final ActivityNotFoundException e) {
Log.d(TAG, "Activity not found", e);
}
}
break;
default:
Log.wtf(TAG, "Unknown action: " + action);
}
}
又看到 startManagePermissionsActivity()
方法又隐式跳转走了。
private void startManagePermissionsActivity() {
// start new activity to manage app permissions
Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "No app can handle android.intent.action.MANAGE_APP_PERMISSIONS");
}
}
无奈,只能点击下图三的 Permissions
(点击后跳转的界面如图六所示),再用adb看看栈顶Activity了,通过 adb shell dumpsys activity top
查看了栈顶Activity,结果如图七所示。
发现栈顶Activity居然是 com.android.packageinstaller/.permission.ui.ManagePermissionsActivity
,包名都不是TV Settings的包名了,已经跳转到另一个应用了,这是我当时没想到的,我还以为权限管理都是写在TV Settings里了呢。好吧,那就只能再去分析另一个包名为 com.android.packageinstaller
应用的源码了。
好在这个应用的源码和TV Settings在一个目录下,如图所示。另一个应用的源码地址(可能需要科学上网): https://android.googlesource.com/platform/packages/apps/PackageInstaller/+/android-6.0.0_r1/src/com/android/packageinstaller/permission/ui/
根据图七的栈顶Activity,找到源码位置,如图九所示。
看下面ManagePermissionsActivity的代码,没多少代码,很好理解,通过
ACTION_MANAGE_APP_PERMISSIONS
,判断是进入了
AppPermissionsFragment
,所以
AppPermissionsFragment
就是图六所示的界面。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
return;
}
Fragment fragment;
String action = getIntent().getAction();
switch (action) {
case Intent.ACTION_MANAGE_PERMISSIONS: {
fragment = ManagePermissionsFragment.newInstance();
} break;
case Intent.ACTION_MANAGE_APP_PERMISSIONS: {
String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
if (packageName == null) {
Log.i(LOG_TAG, "Missing mandatory argument EXTRA_PACKAGE_NAME");
finish();
return;
}
fragment = AppPermissionsFragment.newInstance(packageName);
} break;
case Intent.ACTION_MANAGE_PERMISSION_APPS: {
String permissionName = getIntent().getStringExtra(Intent.EXTRA_PERMISSION_NAME);
if (permissionName == null) {
Log.i(LOG_TAG, "Missing mandatory argument EXTRA_PERMISSION_NAME");
finish();
return;
}
fragment = PermissionAppsFragment.newInstance(permissionName);
} break;
default: {
Log.w(LOG_TAG, "Unrecognized action " + action);
finish();
return;
}
}
getFragmentManager().beginTransaction().replace(android.R.id.content, fragment).commit();
}
那我们在图六中主要关注什么呢?反正我关心的是开关的点击事件,因为在打开、关闭开关的时候源码中做了什么操作,那我就可以学着拿来借用了。那既然是事件,猜测肯定有监听,这都是惯性思维了,所以我就去找开关切换的那部分代码了。哈哈哈,大概浏览了一遍以后,锁定了下面这个方法。这个方法前几行代码是判断性代码,我没太关注,当看到 newValue == Boolean.TRUE
的时候,觉得自己应该是找到地方了,分析了一下源码,感觉应该是通过 group.grantRuntimePermissions(false);
方法授予了权限,通过 group.revokeRuntimePermissions(false);
方法取消了授予的权限。
@Override
public boolean onPreferenceChange(final Preference preference, Object newValue) {
String groupName = preference.getKey();
final AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName);
if (group == null) {
return false;
}
OverlayTouchActivity activity = (OverlayTouchActivity) getActivity();
if (activity.isObscuredTouch()) {
activity.showOverlayDialog();
return false;
}
addToggledGroup(group);
if (LocationUtils.isLocationGroupAndProvider(group.getName(), group.getApp().packageName)) {
LocationUtils.showLocationDialog(getContext(), mAppPermissions.getAppLabel());
return false;
}
if (newValue == Boolean.TRUE) { //打开开关
//给予授权
group.grantRuntimePermissions(false);
} else {//关闭开关
//一个App只有在targetSdkVersion>=23,且当前Android系统版本在6.0以上这两
//个条件同时满足时,才会运行时请求危险权限。
//比如,当一个App的targetSdkVersion<23,但是当前Android系统的版本是7.0,那么该应用会在安装
//时授予权限;当一个App的targetSdkVersion>=23,但是当前Android系统的版本是5.1,那么该应用
//也会在安装时授予权限
//grantedByDefault:该权限组默认被授权
final boolean grantedByDefault = group.hasGrantedByDefaultPermission();
if (grantedByDefault || (!group.hasRuntimePermission() && !mHasConfirmedRevoke)) {
new AlertDialog.Builder(getContext())
.setMessage(grantedByDefault ? R.string.system_warning
: R.string.old_sdk_deny_warning)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.grant_dialog_button_deny,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//关闭权限开关
((SwitchPreference) preference).setChecked(false);
//取消授权
group.revokeRuntimePermissions(false);
if (!grantedByDefault) {
mHasConfirmedRevoke = true;
}
}
})
.show();
return false;
} else {
//取消授权
group.revokeRuntimePermissions(false);
}
}
return true;
}
接下来就是看看grantRuntimePermissions和revokeRuntimePermissions做了哪个关键操作(执行了什么关键方法),才真正地执行了授权和取消授权的操作。
// Grant the permission if needed.
if (!permission.isGranted()) {
permission.setGranted(true);
mPackageManager.grantRuntimePermission(mPackageInfo.packageName,
permission.getName(), mUserHandle);
}
// Revoke the permission if needed.
if (permission.isGranted()) {
permission.setGranted(false);
mPackageManager.revokeRuntimePermission(mPackageInfo.packageName,
permission.getName(), mUserHandle);
}
grantRuntimePermissions和revokeRuntimePermissions方法差不多,我将关键部分的代码摘抄在上面了,看到Google的注释,再通读、理解这两个方法的其它部分,觉得应该是 mPackageManager.revokeRuntimePermission(mPackageInfo.packageName, permission.getName(), mUserHandle);
和mPackageManager.grantRuntimePermission(mPackageInfo.packageName, permission.getName(), mUserHandle);
这两行代码起到了关键作用,毕竟是PackageManager的方法,看着都顺眼,哈哈哈。
public abstract class PackageManager {
......
@SystemApi
public abstract void grantRuntimePermission(@NonNull String packageName,
@NonNull String permissionName, @NonNull UserHandle user);
@SystemApi
public abstract void revokeRuntimePermission(@NonNull String packageName,
@NonNull String permissionName, @NonNull UserHandle user);
......
}
但是有个问题,上面两个方法是系统方法,不对应用层开放,所以,我只能用反射来调用了。调用方法如下所示。
public class PermissionUtil {
private PackageManager getPackageManager() {
return PermissionApplication.getInstance().getPackageManager();
}
/**
* 通过包名和权限名授予运行时权限
*
* @param packageName 包名
* @param permission 权限名
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public void grantPermission(String packageName, String permission) {
try {
PackageManager packageManager = getPackageManager();
Method method = packageManager
.getClass()
.getMethod("grantRuntimePermission", String.class, String.class, UserHandle.class);
method.setAccessible(true);
method.invoke(packageManager, packageName, permission, android.os.Process.myUserHandle());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 通过包名和权限名取消运行时权限的授予
*
* @param packageName 包名
* @param permission 权限名
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public void revokePermission(String packageName, String permission) {
try {
PackageManager packageManager = getPackageManager();
Method method = packageManager
.getClass()
.getMethod("revokeRuntimePermission", String.class, String.class, UserHandle.class);
method.setAccessible(true);
method.invoke(packageManager, packageName, permission, android.os.Process.myUserHandle());
} catch (Exception e) {
e.printStackTrace();
}
}
}
经验证,上面的反射确实能够起到作用,到这里,就解释完了。
再多感慨一句吧,看源码真的觉得有帮助,不仅仅是解决了这个问题,关键看人家的代码结构,人家的API的调用方式都觉得受益良多,甚至看命名都觉得喜欢得不得了。人家的代码写出来,通俗易懂,逻辑还严密,要多多向人家学习了。