Android应用申请运行时权限(Permission)

转载地址:http://blog.csdn.net/gaugamela/article/details/56277793

一直以来,为了保证最大的安全性,安装Android应用时,系统总是让用户选择是否同意该应用所需的所有权限。

一旦安装应用,就意味着该应用所需的所有权限均已获得。 
若在使用某个功能时用到了某个权限,系统将不会提醒用户该权限正在被获取(比如微信需要使用摄像头拍照, 
在Android 6.0以前的设备上,用户将不会被系统告知正在使用“使用系统摄像头”的权限)。

这在安全性上是个隐患:在不经用户同意的情况下,一些应用在后台可以自由地收集用户隐私信息而不被用户察觉。


为了解决这个问题,从Android 6.0版本开始,在安装应用时,该应用无法取得任何权限。 
相反,在使用应用的过程中,若某个功能需要获取某个权限,系统会弹出一个对话框,显式地由用户决定是否将该权限赋予应用。 
只有得到了用户的许可,该功能才可以被使用。

需要注意的是,赋予权限的对话框并不会自动弹出,而需要由开发者手动调用。 
若程序调用的某个方法需要用户赋予相应权限,而此时该权限并未被赋予时,那么程序就会抛出异常并崩溃。 
除此之外,用户还可以在任何时候,通过设置中的应用管理撤销赋予过的权限。

应用的targetSDKVersion < 23时,权限检查仍是早期的形式(仅在安装时赋予权限,使用时将不被提醒); 
应用的targetSDKVersion ≥ 23时,则将使用新的运行时权限规则。


以下罗列了在安装应用时,自动赋予应用的权限,这些权限无法在安装后手动撤销,我们称其为基本权限(Normal Permission)。 
开发者仅需要在AndroidManifest.xml中声明这些权限,应用就能自动获取无需用户授权。

android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

运行时权限被划分成权限组(Permission Group),如下表所示:

若应用被赋予了某个权限组中的一个权限(比如READ_CONTACTS权限被赋予), 
那么该组中的其他权限将被自动获取(WRITE_CONTACTS和GET_ACCOUNTS权限被自动获取)。


检查和申请权限的方法分别是Activity.checkSelfPermission()和Activity.requestPermissions,这两个方法是在 API 23 中新增的。

代码示例如下:

............
mDialButton = (Button) v.findViewById(R.id.crime_dial);
mDialButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= 23) {
            //我是在Fragment里写代码的,因此调用getActivity
            //如果不想判断SDK,可以使用ActivityCompat的接口来检查和申请权限
            int hasReadContactsPermission = getActivity().checkSelfPermission(
                    android.Manifest.permission.READ_CONTACTS);

            if (hasReadContactsPermission != PackageManager.PERMISSION_GRANTED) {
                //这里就会弹出对话框
                getActivity().requestPermissions(
                        new String[] {Manifest.permission.READ_CONTACTS},
                        ASK_READ_CONTACTS_PERMISSION);

                return;
            }

            //高版本中检查是否有运行时权限,具有权限时才调用
            getPhoneNumberAndDial();
        } else {
            //在AndroidManifest.xml中仍然声明使用"android.permission.READ_CONTACTS"
            //在低版本中直接调用该函数
            getPhoneNumberAndDial();
        }
    }
});
............

如前文所述,在Android高版本中,应用初始安装时,即使声明了运行时权限,Android系统也不会为其赋予任何权限。

如下图所示: 

此时,点击按键调用Activity的requestPermissions时,将会弹出对话框,类似于下图所示(不同设备商有不同的定制): 

无论选择的是“允许”还是“拒绝”,系统都将回调Activity.onRequestPermissionsResult()方法, 
并将选择的结果传到方法的第三个参数中。

此时的处理代码示例如下:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case ASK_READ_CONTACTS_PERMISSION:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                getPhoneNumberAndDial();
            } else {
                Toast.makeText(getContext(),
                        "READ_CONTACTS Denied",
                        Toast.LENGTH_SHORT)
                        .show();
            }
            return;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

需要注意的是: 
目前,如果在原生的Fragment中调用Activity.requestPermissions函数时, 
onRequestPermissionsResult必须实现在包含该Fragment的Activity中。

如果是在android.support.v4.app.Fragment中实现相同功能时, 
就可以直接在Fragment中定义onRequestPermissionsResult函数, 
并可以借助于兼容库,此时代码如下:

................
mDialButton = (Button) v.findViewById(R.id.crime_dial);
mDialButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //checkSelfPermission、requestPermissions均是调用兼容库中的函数,此时不再需要判断SDK
        int hasReadContactsPermission = checkSelfPermission(getContext(),
                android.Manifest.permission.READ_CONTACTS);

        if (hasReadContactsPermission != PackageManager.PERMISSION_GRANTED) {
             requestPermissions(
                     new String[] {Manifest.permission.READ_CONTACTS},
                     ASK_READ_CONTACTS_PERMISSION);
             return;
         }

         getPhoneNumberAndDial();
    }
});
..................

关于该问题的讨论可以参考:onRequestPermissionsResult not being called in dialog fragmentAndroid M Permissions: onRequestPermissionsResult() not being called


如上图所示,每当系统申请权限时,弹出的对话框会有一个类似于“拒绝后不再询问”的勾选项。 
若用户打了勾,并选择拒绝,那么下次程序调用Activity.requestPermissions()方法时,将不会弹出对话框,权限也不会被赋予。

这种没有反馈的交互并不是一个好的用户体验。 
所以,下次启动时,程序应弹出一个对话框,提示用户类似于“您已经拒绝了使用该功能所需要的权限,若需要使用该功能,请手动开启权限”的信息, 
此时应调用Activity.shouldShowRequestPermissionRationale()方法,示例代码如下:

............
mDialButton = (Button) v.findViewById(R.id.crime_dial);
mDialButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        int hasReadContactsPermission = checkSelfPermission(getContext(),
                android.Manifest.permission.READ_CONTACTS);

        if (hasReadContactsPermission != PackageManager.PERMISSION_GRANTED) {
            //判断是否点击过“拒绝并不再提示”,若点击了,则应用自己弹出一个Dialog
            if (!shouldShowRequestPermissionRationale(android.Manifest.permission.READ_CONTACTS)) {
                showMessageOKCancel("You need to allow access to Contacts",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                requestPermissions(
                                    new String[] {Manifest.permission.READ_CONTACTS},
                                    ASK_READ_CONTACTS_PERMISSION);
                                }
                            });
                 return;
             }

             requestPermissions(
                     new String[] {Manifest.permission.READ_CONTACTS},
                     ASK_READ_CONTACTS_PERMISSION);

             return;
         }

         getPhoneNumberAndDial();
    }
});
...............
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(getContext())
            .setMessage(message)
            .setPositiveButton("OK", okListener)
            .setNegativeButton("Cancel", null)
            .create()
            .show

此时,应用第一次申请权限及用户勾选了“不再询问”复选框时,均会弹出类似如下的对话框: 

若第一次申请时点击OK,将会弹出权限申请的界面;

用户勾选过“拒绝后不再询问时”,点击OK不会再次拉起申请界面, 
同时onRequestPermissionsResult中收到的结果为PackageManager.PERMISSION_DENIED


最后看看同时申请多个运行时权限的代码示例,其思想基本与前文一致,这段代码是直接借鉴过来的:

final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;

private void insertDummyContactWrapper() {
    //提示用户需要手动开启的权限集合
    List<String> permissionsNeeded = new ArrayList<String>();

    //功能所需权限的集合
    final List<String> permissionsList = new ArrayList<String>();

    //若用户拒绝了该权限申请,则将该申请的提示添加到“用户需要手动开启的权限集合”中
    if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
        permissionsNeeded.add("GPS");
    if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
        permissionsNeeded.add("Read Contacts");
    if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
        permissionsNeeded.add("Write Contacts");

    //存在未配置的权限
    if (permissionsList.size() > 0) {

        //若用户赋之前拒绝过一部分权限,则需要提示用户开启其余权限并返回,否则该功能将无法执行
        if (permissionsNeeded.size() > 0) {

            // Need Rationale
            String message = "You need to grant access to ";
            for (int i = 0; i < permissionsNeeded.size(); i++)
                message = message + ", " + permissionsNeeded.get(i);

                //弹出对话框,提示用户需要手动开启的权限
                showMessageOKCancel(message,
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                    REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                        }
                    });
            return;
        }

        requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
        return;
    }

    insertDummyContact();
}

//判断用户是否授予了所需权限 
private boolean addPermission(List<String> permissionsList, String permission) {
    //若配置了该权限,返回true
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        //若未配置该权限,将其添加到所需权限的集合,返回true
        permissionsList.add(permission);

        // 若用户勾选了“永不询问”复选框,并拒绝了权限,则返回false
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }

    return true;
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
            //初始化Map集合,其中Key存放所需权限,Value存放该权限是否被赋予
            Map<String, Integer> perms = new HashMap<String, Integer>();

            // 向Map集合中加入元素,初始时所有权限均设置为被赋予(PackageManager.PERMISSION_GRANTED)
            perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);

            // 将第二个参数回传的所需权限及第三个参数回传的权限结果放入Map集合中,由于Map集合要求Key值不能重复,所以实际的权限结果将覆盖初始值
            for (int i = 0; i < permissions.length; i++)
                perms.put(permissions[i], grantResults[i]);

                // 若所有权限均被赋予,则执行方法
                if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                    // All Permissions Granted
                    insertDummyContact();
                } 
                //否则弹出toast,告知用户需手动赋予权限
                else {
                    // Permission Denied
                    Toast.makeText(MainActivity.this, 
                        "Some Permission is Denied",
                        Toast.LENGTH_SHORT)
                        .show();
                }
            }
            break;

        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

P.S.

根据permission名称,获取PermissionInfo的代码类似于:

................
    public static List<PermissionInfo> getPermissionInfos(Context context,
            String... permissions) {
        List<PermissionInfo> rst = new ArrayList<>();

        PackageManager pm = context.getPackageManager();

        try {
            for (String name : permissions) {
                rst.add(pm.getPermissionInfo(name, 0));
            }
        } catch (PackageManager.NameNotFoundException e) {
        }

        return rst;
    }
..............

PermissionInfo中主要保存Permission对应的ProtectionLevel。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值