Android权限

  • 系统权限
  • 在运行时请求权限

系统权限

  Android是一个权限分离的操作系统,每个应用使用不同的系统身份运行(Linux用户ID和群组ID)。系统的不同部分也被分隔进不同的身份域。Linux以此来实现不同应用与其他应用和系统的独立。
  更细粒度的安全特征由“权限”机制提供,该机制在一个进程能够执行的特定操作上强制施加了限制,并且每一个URI权限能够授予对特定数据的临时访问。

安全框架

  Android安全框架的一个中心设计点是,默认情况下,没有应用具有执行任何不利地影响其他应用,操作系统或用户的操作的权限。这些操作包括,读写用户私有数据,读写其他应用程序文件,执行网络访问,保持设备唤醒等等。
  因为每一个Android程序运行在进程沙盒里,所以应用间必须严格地分享资源和数据。它们能够通过声明它们需要的权限来增加基础沙盒没有提供的能力。应用声明并请求它们需要的权限,Android系统提示用户以获得用户赞同。
  应用程序沙盒不依赖用来构建应用的技术。实际上,Dalvik VM不是一个安全的边界,任何应用都能运行本地代码。所有类型的应用-Java,native,hybrid-是使用相同的方式被沙盒封装,他们有相同的安全等级。

应用签名

  所有的apk必须使用证书签名,该证书的私有密钥由开发者持有。该证书标识了应用的作者。该证书不需要被一个证书颁发机构签名。并且对于Android应用使用自签名证书,是非常被允许和支持的。在Android中证书的作用是用来区分应用的作者。

用户IDs和文件访问

  在安装时,Android会给每一个安装包一个不同的Linux用户ID。这个身份在该应用在设备上的期间是恒定不变的。在不同的设备上,相同的安装包可有有不同的UID。
  安全限制发生在进程级别。由于不同应用需要运行作为不同的Linux用户,所以两个不同安装包的代码通常不能运行在相同的进程。但你可以在AndroidManifest.xml的manifest标签中使用sharedUserId属性来让不同的包分配相同的用户ID。通过这样做,两个包是被作为相同的应用,有相同的用户ID和文件权限。注意为了确保安全,仅两个使用相同签名,且请求相同sharedUserId的应用会被分配相同的用户ID。
  一个应用存储的任何数据都会被分配应用的用户ID,并且正常情况下不能被其他的包访问。当使用getSharedPreferences(String, int)openFileOutput(String, int)openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory)等方法创建新文件时,你能够使用MODE_WORLD_READABLEMODE_WORLD_WRITEABLE标签来允许其他应用读写该文件。当设置这些标签时,该文件仍然被你的应用所拥有,但它的全局读写权限已经被适当地设置,所以其它应用可以看见它。

使用权限

  默认情况下,一个基本的Android应用没有被分配任何权限,这意味着它不能做任何不利地影响用户体验和数据的操作。为了使用设备上受保护的特征,你必须在AndroidManifest.xml中使用一个或多个<uses-permission>标签。
例如,一个应用需要接收SMS信息,则这样定义:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.app.myapp" >
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>

  如果你的应用声明了“正常权限”(不会对用户隐私或设备造成影响的操作权限),系统会自动授予这些权限。如果你的应用声明了“危险权限”(会对用户隐私或设备造成影响的操作权限),系统会询问用户严格地授予这些权限。请求权限的表现形式具体依赖于设备系统版本和应用的目标系统版本:

  • 如果你的设备运行在Android6.0(API级别23)以上,并且你的应用的targetSdkVersion是23或更高,则该应用在运行时向用户请求权限。用户可以在任何时候取消这个权限,所以应用需要在每次运行时检测是否已有该权限。
  • 如果你的设备运行在Android5.1(API级别22)一下,会你的应用的targetSdkVersion是22或更低,则系统在安装应用时,询问用户授予该权限。如果你在更新的版本中增加了一个新权限,则系统在升级应用时,询问用户授予该权限。一旦用户安装了该应用,唯一的取消该权限的方式就是卸载应用。

通常一个权限失败会导致抛出一个SecurityException。然而,这不是绝对的。例如,sendBroadcast(Intent)方法在数据被传递到每一个接收器时检查权限,当方法调用返回后,如果权限失败,不会收到异常。在几乎所有情况下,一个权限失败会打印一条log。
  Android系统提供的所有权限可以在Manifest.permission中找到。应用也可以定义自己的权限。
  通常特定的权限被施加在你的应用的有数的几个位置上:

  • 调入系统时,防止应用执行特定功能。
  • 当启动一个Activity时,防止应用创建其他应用的Activity。
  • 发送或接受广播时,来控制谁能接收你的广播或谁能发送广播给你。
  • 当在Content Provider上访问和操作时。
  • 绑定或启动服务时。

自动权限调整

  随着时间的推移,新的限制会被增加到平台,为了使用这些APIs,你的应用必须请求它以前不需要的权限。如果已发布的应用访问了这些API会怎么呢?Android会让其自由的访问,并在新平台版本上将请求的新权限授予应用,以防止已发布的应用挂掉。Android通过targetSdkVersion属性提供的值来判断是否需要请求新权限。如果这个值低于新权限被增加的版本,则Android默认授予该权限。
  例如,WRITE_EXTERNAL_STORAGE权限是在API级别4增加的用来限制访问共享存储空间的权限。如果你的targetSdkVersion设置的值小于等于3,则该权限在新Android版本上默认被增加到你的应用。
  注意:被自动增加到App的权限,会被列在Google Play权限列表中,尽管你的应用不需要请求他们。
  为了避免这些并移除你不需要的默认权限,一直尽可能高的更新你的targetSdkVersion。你可以在Build.VERSION_CODES文档中找到某个权限是在哪个版本中新增的。

“正常”和“危险”权限

  系统权限被分为几个不同的保护级别。两个最重要的保护级别是正常”和“危险”权限:

  • “正常权限”覆盖了你的应用需要访问的数据或资源是应用沙盒外的所有区域,在这些区域对用户隐私或其他应用程序的操作所造成的风险是非常小的。例如,设置时间地区的权限是一个 “正常”权限。如果应用声明了它需要一个“正常”权限,则系统自动授予应用该权限。
  • “危险权限”覆盖了所有你的应用想要的数据或资源包含用户的隐私信息,或可能潜在地影响用户存储的数据或其他应用的操作的区域。例如,读取用户的联系人的权限就是一个危险的权限。如果应用声明了它想要一个“危险”权限,则用户必须严格授予该权限给应用。

当前所有的“正常权限”

PROTECTION_NORMAL类权限
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS

权限组

  所有“危险权限”属于权限组。如果设备正运行在Android6.0(API级别23)之上,并且该应用的targetSdkVersion是23或更高,则当应用请求一个“危险权限”时,下面的系统行为出现。

  • 如果应用请求了一个“危险权限”,且应用当前没有该权限所在权限组的权限,则系统展示一个对话框去向用户描述该应用想要访问的权限组。这个对话框不能描述该权限组中的具体权限。例如,如果应用仅请求了READ_CONTACTS权限,则系统对话框仅会展示该应用需要访问设备的联系人。如果用户授予了该权限,则系统仅会授予该应用请求的权限,同一权限组中的其他权限还需要再去请求。
  • 如果应用请求了一个“危险权限”,并且该应用已经有了该权限组中的另一个“危险权限”,则系统会立刻授予该权限,而不会与用户有任何的交互。例如,如果应用已经请求并授予了READ_CONTACTS权限,则当它再请求WRITE_CONTACTS权限时,系统会立刻授予该权限。

注意:任何权限都属于一个权限组,包括“正常权限”和用户自定义的权限。只是“危险权限”的权限组会影响用户体验,所以我们特殊对待。

所有“危险权限”和权限组

权限组 权限
CALENDAR READ_CALENDAR
WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
SENSORS BODY_SENSORS
SMS SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
STORAGE READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE

定义和施行权限

  为了施行自己的权限,你首先需要在AndroidManifest.xml中使用<permission>标签定义权限。
  例如,应用想要控制谁能启动一个Activity,则可以像下面这样定义权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.me.app.myapp" >
    <permission android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
        android:label="@string/permlab_deadlyActivity"
        android:description="@string/permdesc_deadlyActivity"
        android:permissionGroup="android.permission-group.COST_MONEY"
        android:protectionLevel="dangerous" />
    ...
    <uses-permission android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY" />
    . . .
    <application . . .>
        <activity android:name=".FreneticActivity"
                  android:permission="com.me.app.myapp.permission.DEADLY_ACTIVITY"
                  . . . >
            . . .
        </activity>
    </application>
</manifest>
  • protectionLevel:指定了该权限的安全级别,它告诉系统当应用请求该权限时怎样通知用户,或谁被允许持有该权限。
  • permissionGroup:其仅帮助系统向用户显示权限。通常你设置其为一个标准系统权限组(android.Manifest.permission_group中列举)或自定义的权限组。
  • label:被用来展示给用户当展示一个权限列表时,要尽量简短
  • description:具体描述一个权限

可以在手机“设置”->“应用程序”中选择具体的应用来查看其需要的相关权限,也可以使用adb shell pm list permissions命令查看权限:

$ adb shell pm list permissions -s
All Permissions:

Network communication: view Wi-Fi state, create Bluetooth connections, full
Internet access, view network state

Your location: access extra location provider commands, fine (GPS) location,
mock location sources for testing, coarse (network-based) location

Services that cost you money: send SMS messages, directly call phone numbers
...

在运行时请求权限

  从Android6.0(API级别23)开始,用户在应用运行时授予权限。这种方式简化了应用的安装过程,因为用户不再需要在安装或升级应用时授予权限。这也给了用户在使用应用过程中的更多控制。例如,一个用户能够选择给一个相机应用访问相机的权限而不给获取设备位置的权限。用户能够随时在应用设置界面取消授予的权限。

检测权限

  如果你的应用需要一个“危险权限”,则在每次执行请求这个权限的操作时,你必须检测你是否已经被授予该权限。因为用户可以随时取消授权。
使用ContextCompat.checkSelfPermission()方法来检测你是否被授予一个权限。例如,下面的代码段展示了怎样检测一个Activity是否已被授予写日历的权限:

// Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);

  如果应用具有该权限,则该方法返回PackageManager.PERMISSION_GRANTED,则该应用可以执行该操作。如果该应用不具有该权限,则该方法返回PERMISSION_DENIED,则应用不得不严格地向用户请求该权限。

请求权限

  如果你的应用需要一个“危险权限”,它必须询问用户授予该权限。Android提供了几种用来请求权限的方法。调用这些方法调起一个标准的对话框,它不能被订制。

解释为什么应用需要这个权限

  有些情况下,你可能想帮用户理解为什么你的应用需要一个权限。例如,如果启动一个拍照应用,则用户不会惊讶于应用请求使用相机的权限,但用户不能理解应用为什么想要访问用户的位置和联系人。在你请求一个权限之前,你应该考虑向用户做出一个解释。注意不要让解释影响了用户。如果你提供了太多的解释,用户可能会产生厌烦,卸载了你的应用。
  一种你能够使用的方法是,仅当用户已经拒绝了权限请求时提供解释。如果用户持续尝试使用一个请求权限的功能,但却保持拒绝权限请求,则这可能表明用户不理解为什么应用需要该权限来提供这个功能。像这种情况,可能给出一个解释是个好的实践。
  为了帮助找出用户可能需要一个解释的情况,Android提供了一个实用的方法shouldShowRequestPermissionRationale()。如果应用之前请求过这个权限并且用户拒绝了该请求,则该方法返回true。
  注意:如果用户在过去拒绝了这个权限请求并在该权限的请求对话框中选择了“不再询问”选项,则该方法返回false。如果设备策略禁止该应用有该权限则该方法也返回false。

请求你需要的权限

  如果你的应用还没有它需要的权限,则该应用必须调用requestPermissions()方法来请求该权限。该方法需要传递你的应用需要的权限和标识该次权限请求的一个整形请求码。该方法的作用是异步的,当用户响应该对话框后,系统调用回调方法返回授权结果,结果中会包含请求权限时传入的相同请求码。
  下面的代码检测应用是否具有读用户联系人的权限并在需要时请求该权限:

// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {

    // Should we show an explanation?
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.READ_CONTACTS)) {

        // Show an expanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.

    } else {

        // No explanation needed, we can request the permission.

        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

        // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
        // app-defined int constant. The callback method gets the
        // result of the request.
    }
}

处理权限请求响应

  当你的应用请求权限时,系统呈现一个对话框给用户。当用户响应时,系统触发onRequestPermissionsResult()方法,给出响应结果。下面给出了请求READ_CONTACTS权限时的响应回调方法:

Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // permission was granted, yay! Do the
                // contacts-related task you need to do.

            } else {

                // permission denied, boo! Disable the
                // functionality that depends on this permission.
            }
            return;
        }

        // other 'case' lines to check for other
        // permissions this app might request
    }
}

  注意:你的应用仍需要严格地请求每一个它需要的权限,尽管用户已经授予了相同权限组的其他权限。因为权限组的分组可能会在将来的某个Android版本更改。你的代码不应该依赖该权限是不是在相同权限组的假设。
  当系统询问用户授予权限时,用户有一个操作选项用来告诉系统需要再次询问授予该权限。在这种情况下,每次使用requestPermissions()方法请求权限,系统都会立即拒绝请求,而不再弹出授权对话框。

运行时权限示例程序

使用Android原生API请求权限

  Android API中处理运行时权限的方法checkSelfPermission()shouldShowRequestPermissionRationale()requestPermissions()等都是在Android6.0版本增加的方法,所以要使用这些方法必须要至少API级别为23,为了调用方法的兼容性可以通过如下检查运行版本的方式处理:

if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}

  这种方法虽然可以解决兼容性问题,但太过复杂,幸运的是,v4兼容库已经提供了相应的兼容方法。

  • ContextCompat.checkSelfPermission()
    被授权函数返回PERMISSION_GRANTED,否则返回PERMISSION_DENIED,在所有版本都是如此。
  • ActivityCompat.requestPermissions()
    这个方法在M之前版本调用,OnRequestPermissionsResult回调方法直接被调用,带着正确的PERMISSION_GRANTED或者PERMISSION_DENIED
  • ActivityCompat.shouldShowRequestPermissionRationale()
    在M之前版本调用,永远返回false。

后两种方法,在Fragment中也有相应方法,使用v13兼容包的FragmentCompat.requestPermissions()FragmentCompat.shouldShowRequestPermissionRationale()方法。

一次请求一个权限

  以拍照功能为例,我们示例请求拍照权限。
在AndroidManifest.xml中声明权限

<uses-permission android:name="android.permission.CAMERA" />

在SingleNormalActivity.java中动态请求权限

/**
 * 单个权限请求处理
 * Created by sunxiaodong on 16/4/26.
 */
public class SingleNormalActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int CAMERA_REQUEST_CODE = 1;
    private static final int PERMISSION_REQUEST_CODE = 2;

    private Button mGoCamera;
    private ImageView mPhotoImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_activity);
        initView();
    }

    private void initView() {
        mGoCamera = (Button) findViewById(R.id.go_camera);
        mGoCamera.setOnClickListener(this);
        mPhotoImageView = (ImageView) findViewById(R.id.photo_imageview);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.go_camera:
                getCameraPermission();
                break;
        }
    }

    private void getCameraPermission() {
        int hasCameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
        if (hasCameraPermission != PackageManager.PERMISSION_GRANTED) {
            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
                showRationaleDialog(getResources().getString(R.string.permission_camera_rationale),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                ActivityCompat.requestPermissions(SingleNormalActivity.this,
                                        new String[]{Manifest.permission.CAMERA},
                                        PERMISSION_REQUEST_CODE);
                            }
                        });
                return;
            }
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.CAMERA},
                    PERMISSION_REQUEST_CODE);
            return;
        }
        camera();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_CODE:
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    camera();
                } else {
                    Toast.makeText(this, getResources().getString(R.string.permission_camera_denied), Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    private void camera() {
        Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        startActivityForResult(openCameraIntent, CAMERA_REQUEST_CODE);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case CAMERA_REQUEST_CODE:
                    Bitmap bm = (Bitmap) data.getExtras().get("data");
                    mPhotoImageView.setImageBitmap(bm);
                    break;
                default:
                    break;
            }
        }
    }

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

}

下面给出授权过程的一个执行路径
  首次点击拍照按钮,弹出自定义授权解释对话框:
这里写图片描述

  点击授权解释对话框的“允许”选项,弹出系统的授权请求对话框:
这里写图片描述

  点击授权请求对话框的“拒绝”选项后,再次点击拍照按钮,弹出有“不再询问”选项的授权请求对话框:
这里写图片描述

  选择“不再询问”选项,点击授权请求对话框的“拒绝”选项后,再次点击拍照按钮,弹出自定义授权解释对话框:
这里写图片描述

  点击授权解释对话框的“允许”选项,不再有任何反应。

  分析程序中可知,在运行时权限下,如果想要使用拍照功能,就必须在使用相机功能前,主动请求相机权限,程序中,我们首先调用ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)方法获取应用是否已经被授予相机权限,如果已被授予该权限,则可直接使用相机功能,否则通过调用ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)来判断用户是否拒绝过该权限的授予或是否用户已经选择了“不再询问”选项予以拒绝授予该权限,如果用户已经选择了“不再询问”选项予以拒绝授予该权限,则系统不再弹出权限请求提示框,所以我们自己弹出需要相机权限的解释,如果仅是简单地被拒绝了授予权限,则再次调用ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CODE);方法请求权限。请求权限后,用户响应授权提示的结果,通过onRequestPermissionsResult()回调方法返回,接下来根据用户授权情况,做出相应处理。

一次请求多个权限

  接下来,在请求拍照功能的同时,一起请求联系人权限。
在AndroidManifest.xml中声明权限

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />

在MultiNormalActivity.java中动态请求权限

/**
 * 常规的一次请求多个权限处理方式
 * Created by sunxiaodong on 16/4/26.
 */
public class MultiNormalActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int CAMERA_REQUEST_CODE = 1;
    private static final int PERMISSION_REQUEST_CODE = 2;

    private Button mGoCamera;
    private ImageView mPhotoImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_activity);
        initView();
    }

    private void initView() {
        mGoCamera = (Button) findViewById(R.id.go_camera);
        mGoCamera.setOnClickListener(this);
        mPhotoImageView = (ImageView) findViewById(R.id.photo_imageview);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.go_camera:
                getCameraAndContactsPermission();
                break;
        }
    }

    private boolean addPermission(List<String> permissionsList, String permission) {
        if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
            permissionsList.add(permission);
            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission))
                return false;
        }
        return true;
    }

    private void getCameraAndContactsPermission() {
        List<String> permissionsNeeded = new ArrayList<String>();
        final List<String> permissionsList = new ArrayList<String>();
        if (!addPermission(permissionsList, Manifest.permission.CAMERA)) {
            permissionsNeeded.add("相机");
        }
        if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS)) {
            permissionsNeeded.add("读联系人");
        }
        if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS)) {
            permissionsNeeded.add("写联系人");
        }
        if (permissionsList.size() > 0) {
            if (permissionsNeeded.size() > 0) {
                String message = getResources().getString(R.string.permission_rationale, permissionsNeeded.get(0));
                for (int i = 1; i < permissionsNeeded.size(); i++)
                    message = message + ", " + permissionsNeeded.get(i);
                showRationaleDialog(message,
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                ActivityCompat.requestPermissions(MultiNormalActivity.this, permissionsList.toArray(new String[permissionsList.size()]),
                                        PERMISSION_REQUEST_CODE);
                            }
                        });
                return;
            }
            ActivityCompat.requestPermissions(this, permissionsList.toArray(new String[permissionsList.size()]),
                    PERMISSION_REQUEST_CODE);
            return;
        }
        camera();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_CODE: {
                Map<String, Integer> perms = new HashMap<String, Integer>();
                perms.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
                perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
                for (int i = 0; i < permissions.length; i++)
                    perms.put(permissions[i], grantResults[i]);
                if (perms.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
                        && perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                    camera();
                } else {
                    Toast.makeText(this, getResources().getString(R.string.permission_denied), Toast.LENGTH_SHORT).show();
                }
            }
            break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    private void camera() {
        Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        startActivityForResult(openCameraIntent, CAMERA_REQUEST_CODE);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case CAMERA_REQUEST_CODE:
                    Bitmap bm = (Bitmap) data.getExtras().get("data");
                    mPhotoImageView.setImageBitmap(bm);
                    break;
                default:
                    break;
            }
        }
    }

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

}

  从程序中可知,一次请求多个权限的过程同一次请求一个权限的过程,所以不再对其进行分析,读者可下载示例源码自行查看结果。

使用PermissionDispatcher库请求权限

  使用Android原生API请求权限有些复杂,所以出现了很多第三方库来简化过程,PermissionDispatcher是其中一个比较好用的库,接下来对使用其进行权限请求进行介绍。
继续以拍照为例。

一次请求一个权限

在AndroidManifest.xml中声明权限

<uses-permission android:name="android.permission.CAMERA" />

在SinglePermissionDispatcherActivity.java中动态请求权限

/**
 * 使用PermissionDispatcher处理一次单个权限请求
 * Created by sunxiaodong on 16/4/26.
 */
@RuntimePermissions
public class SinglePermissionDispatcherActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int CAMERA_REQUEST_CODE = 1;

    private Button mGoCamera;
    private ImageView mPhotoImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_activity);
        initView();
    }

    private void initView() {
        mGoCamera = (Button) findViewById(R.id.go_camera);
        mGoCamera.setOnClickListener(this);
        mPhotoImageView = (ImageView) findViewById(R.id.photo_imageview);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.go_camera:
                SinglePermissionDispatcherActivityPermissionsDispatcher.cameraWithCheck(this);
                break;
        }
    }

    @NeedsPermission(Manifest.permission.CAMERA)
    public void camera() {
        Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        startActivityForResult(openCameraIntent, CAMERA_REQUEST_CODE);
    }

    @OnShowRationale(Manifest.permission.CAMERA)
    void showRationaleForCamera(PermissionRequest request) {
        showRationaleDialog(R.string.permission_camera_rationale, request);
    }

    @OnPermissionDenied(Manifest.permission.CAMERA)
    void onCameraDenied() {
        Toast.makeText(this, R.string.permission_camera_denied, Toast.LENGTH_SHORT).show();
    }

    @OnNeverAskAgain(Manifest.permission.CAMERA)
    void onCameraNeverAskAgain() {
        Toast.makeText(this, R.string.permission_camera_never_askagain, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        SinglePermissionDispatcherActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case CAMERA_REQUEST_CODE:
                    Bitmap bm = (Bitmap) data.getExtras().get("data");
                    mPhotoImageView.setImageBitmap(bm);
                    break;
                default:
                    break;
            }
        }
    }

    private void showRationaleDialog(@StringRes int messageResId, final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setPositiveButton(R.string.button_allow, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        request.proceed();
                    }
                })
                .setNegativeButton(R.string.button_deny, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        request.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage(messageResId)
                .show();
    }

}

下面给出授权过程的一个执行路径
  首次点击拍照按钮,弹出系统的授权请求对话框:
这里写图片描述

  点击授权请求对话框的“拒绝”选项后,再次点击拍照按钮,弹出自定义授权解释对话框:
这里写图片描述

  点击自定义授权解释对话框的“允许”选项后,弹出有“不再询问”选项的授权请求对话框:
这里写图片描述

  选择“不再询问”选项,点击授权请求对话框的“拒绝”选项后,再次点击拍照按钮,弹出授权不再询问的Toast提示:
这里写图片描述

一次请求多个权限

在AndroidManifest.xml中声明权限

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />

在MultiPermissionDispatcherActivity.java中动态请求权限

/**
 * 使用PermissionDispatcher处理一次多个权限请求
 * Created by sunxiaodong on 16/4/26.
 */
@RuntimePermissions
public class MultiPermissionDispatcherActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int CAMERA_REQUEST_CODE = 1;

    private Button mGoCamera;
    private ImageView mPhotoImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.camera_activity);
        initView();
    }

    private void initView() {
        mGoCamera = (Button) findViewById(R.id.go_camera);
        mGoCamera.setOnClickListener(this);
        mPhotoImageView = (ImageView) findViewById(R.id.photo_imageview);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.go_camera:
                MultiPermissionDispatcherActivityPermissionsDispatcher.cameraWithCheck(this);
                break;
        }
    }

    @NeedsPermission({Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
    public void camera() {
        Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        startActivityForResult(openCameraIntent, CAMERA_REQUEST_CODE);
    }

    @OnShowRationale({Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
    void showRationaleForCamera(PermissionRequest request) {
        showRationaleDialog(R.string.permission_camera_contacts_rationale, request);
    }

    @OnPermissionDenied({Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
    void onCameraDenied() {
        Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_SHORT).show();
    }

    @OnNeverAskAgain({Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
    void onCameraNeverAskAgain() {
        Toast.makeText(this, R.string.permission_never_askagain, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        MultiPermissionDispatcherActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case CAMERA_REQUEST_CODE:
                    Bitmap bm = (Bitmap) data.getExtras().get("data");
                    mPhotoImageView.setImageBitmap(bm);
                    break;
                default:
                    break;
            }
        }
    }

    private void showRationaleDialog(@StringRes int messageResId, final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setPositiveButton(R.string.button_allow, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        request.proceed();
                    }
                })
                .setNegativeButton(R.string.button_deny, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        request.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage(messageResId)
                .show();
    }

}

  从程序中可知,使用PermissionDispatcher一次请求多个权限的过程同一次请求一个权限的过程,所以不再对其进行分析,读者可下载示例源码自行查看结果。

源码地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章