Gerenciamento de Permissões em Tempo de Execução no Android Usando Métodos grantRuntimePermission e revokeRuntimePermission

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.

Tags: android Permissões em Tempo de Execução PackageManager Reflexão java

Publicado em 6-11 19:42 por Thomas