Objetivo
Obter permissões em tempo de execução como READ_PHONE_STATE sem interação direta do usuário, simulando a ação de conceder permissão através de código.
Análise e Abordagem
Para implementar essa funcionalidade, é necessário entender como o sistema Android gerencia permisões. A ideia é inspecionar o código-fonte do instalador de pacotes para simular a ação de um usuário concdeendo ou revogando permissões.
Passo 1: Identificação da Atividade Relevante
Usando o comando adb shell dumpsys activity | grep "mFocusedActivity", podemos determinar a atividade atual, que no caso é com.android.packageinstaller/.permission.ui.ManagePermissionsActivity. Isso nos leva a examinar o código-fonte do instalador de pacotes.
Passo 2: Exame do Código-Fonte
No ManagePermissionsActivity, o fragmento AppPermissionsFragment lida com mudanças de permissão. O método onPreferenceChange é responsável por conceder ou revogar permissões com base na interação do usuário.
// Código reescrito do AppPermissionsFragment
@Override
public boolean onPreferenceChanged(Preference pref, Object updatedValue) {
String permissionGroupName = pref.getKey();
final AppPermissionGroup permissionGroup = mAppPermissions.getPermissionGroup(permissionGroupName);
if (permissionGroup == null) {
return false;
}
addToggledGroup(permissionGroup);
if (LocationUtils.isLocationGroupAndProvider(permissionGroup.getName(), permissionGroup.getApp().packageName)) {
LocationUtils.showLocationDialog(getContext(), mAppPermissions.getAppLabel());
return false;
}
if (updatedValue == Boolean.TRUE) {
permissionGroup.grantRuntimePermissions(false);
} else {
final boolean defaultGranted = permissionGroup.hasGrantedByDefaultPermission();
if (defaultGranted || (!permissionGroup.hasRuntimePermission() && !mHasConfirmedRevoke)) {
new AlertDialog.Builder(getContext())
.setMessage(defaultGranted ? R.string.system_warning : R.string.old_sdk_deny_warning)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.grant_dialog_button_deny_anyway,
(dialog, which) -> {
((SwitchPreference) pref).setChecked(false);
permissionGroup.revokeRuntimePermissions(false);
if (!defaultGranted) {
mHasConfirmedRevoke = true;
}
})
.show();
return false;
} else {
permissionGroup.revokeRuntimePermissions(false);
}
}
return true;
}
Passo 3: Métodos de Concessão e Revogação
No AppPermissionGroup, os métodos grantRuntimePermissions e revokeRuntimePermissions interagem com o PackageManager do sistema. Para aplicações que suportam permissões em tempo de execução, esses métodos são chamados diretamente.
// Código reescrito do AppPermissionGroup - método de concessão
public boolean grantRuntimePermissions(boolean userFixed, String[] permissionsFilter) {
final int applicationUid = mPackageInfo.applicationInfo.uid;
for (Permission perm : mPermissions.values()) {
if (permissionsFilter != null && !ArrayUtils.contains(permissionsFilter, perm.getName())) {
continue;
}
if (mAppSupportsRuntimePermissions) {
if (perm.isSystemFixed()) {
return false;
}
if (perm.hasAppOp() && !perm.isAppOpAllowed()) {
perm.setAppOpAllowed(true);
mAppOps.setUidMode(perm.getAppOp(), applicationUid, AppOpsManager.MODE_ALLOWED);
}
if (!perm.isGranted()) {
perm.setGranted(true);
mPackageManager.grantRuntimePermission(mPackageInfo.packageName,
perm.getName(), mUserHandle);
}
if (!userFixed) {
if (perm.isUserFixed() || perm.isUserSet()) {
perm.setUserFixed(false);
perm.setUserSet(false);
mPackageManager.updatePermissionFlags(perm.getName(),
mPackageInfo.packageName,
PackageManager.FLAG_PERMISSION_USER_FIXED
| PackageManager.FLAG_PERMISSION_USER_SET,
0, mUserHandle);
}
}
} else {
if (!perm.isGranted()) {
continue;
}
int uidToKill = -1;
int flagsMask = 0;
if (perm.hasAppOp()) {
if (!perm.isAppOpAllowed()) {
perm.setAppOpAllowed(true);
mAppOps.setUidMode(perm.getAppOp(), applicationUid, AppOpsManager.MODE_ALLOWED);
uidToKill = applicationUid;
}
if (perm.shouldRevokeOnUpgrade()) {
perm.setRevokeOnUpgrade(false);
flagsMask |= PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE;
}
}
if (flagsMask != 0) {
mPackageManager.updatePermissionFlags(perm.getName(),
mPackageInfo.packageName, flagsMask, 0, mUserHandle);
}
if (uidToKill != -1) {
mActivityManager.killUid(applicationUid, KILL_REASON_APP_OP_CHANGE);
}
}
}
return true;
}
Passo 4: Implementação via Reflexão
Os métodos grantRuntimePermission e revokeRuntimePermission no PackageManager são anotados com @SystemApi, exigindo privilégios de sistema. É possível acessá-los usando reflexão em versões do Android >= Jelly Bean MR1.
// Código reescrito para chamada via reflexão
if (parameters != null) {
try {
String packageName = parameters.getString(KEY_PACKAGE_NAME);
String permName = parameters.getString(PERMISSION_NAME);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
PackageManager pkgManager = mContext.getPackageManager();
Method grantMethod = pkgManager.getClass().getDeclaredMethod(
"grantRuntimePermission", String.class, String.class, UserHandle.class);
grantMethod.setAccessible(true);
grantMethod.invoke(pkgManager, packageName, permName, Process.myUserHandle());
} else {
LogUtils.e(TAG, "Versão do Android incompatível");
}
} catch (Exception e) {
LogUtils.e(TAG, "Erro ao acessar métodos de permissão");
e.printStackTrace();
}
}
Considerações Importantes
Ao usar grantRuntimePermission, o nome da permissão pode ser Manifest.permission.READ_PHONE_STATE. No entanto, para revokeRuntimePermission, é necessário usar o nome completo como android.permission.READ_PHONE_STATE. Além disso, revogar uma permissão em tempo de execução força o encerramento imediato do aplciativo afetado, como indicado nos logs do sistema.