天天看点

运行时权限

在Android开发中经常需要各种各样的权限,通常我们只需要在

AndroidManifest.xml

中声明一下需要的权限即可。但是自从Android6.0(即 API Level 23)开始,Android系统将权限的授予从安装时,变成了运行时。这样可以提高APP的安装速度和提高用户对于权限的控制。

Normal And Dangerous

现在我们来看一下这段话:

System permissions are divided into two categories, normal and dangerous:
  • Normal permissions do not directly risk the user’s privacy. If your app lists a normal permission in its manifest, the system grants the permission automatically.
  • Dangerous permissions can give the app access to the user’s confidential data. If your app lists a normal permission in its manifest, the system grants the permission automatically. If you list a dangerous permission, the user has to explicitly give approval to your app.

简而言之,就是

normal

的就是不会涉及用户隐私的权限,它是在你需要用的时候,系统自动授权的,

dangerous

的是会设计用户隐私的权限,他需要用户显示的授权给你的APP。

那接下来让我们来看一下

dangerous

的权限都是哪些:

Permission Group Permissions

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

这些分别对应着日历、相机、通讯录、位置、麦克风、电话、传感器、短信和外部存储。

由于这些信息都是可以直接或者间接的去获取到用户的隐私所以google将它设置为dangerous不是没有道理的,相信这个可以在一定程度上改善国内应用获取权限过多的情况。

获取dangerous权限

我们讲了那么多关于dangerous权限,现在就让我们来看看如何去获取dangerous权限。

现在我们来尝试获取一下Google Galaxy Nexus Android 4.4.2 API Level 17的设备的通讯录。

运行时权限

里面只有一个联系人,现在我们先要来获取一下该联系人,然后插入另一个联系人

private void readContacts() {

    Cursor cursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI, new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
                ContactsContract.Data.DATA1}, null, null, null);
    cursor.moveToFirst();
    Log.d(TAG, "readContacts: name: " + cursor.getString() + " ,number: " + cursor.getString());

}

private void writeContacts() {                                                              

    ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();    
    int rawContactInsertIndex = ops.size();                                                 

    ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)    
            .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)                     
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build());           

    //Phone Number                                                                          
    ops.add(ContentProviderOperation                                                        
            .newInsert(ContactsContract.Data.CONTENT_URI)                                   
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,                   
                    rawContactInsertIndex)                                                  
            .withValue(ContactsContract.Data.MIMETYPE,                                      
                    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)               
            .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "987654321")          
            .withValue(ContactsContract.Data.MIMETYPE,                                      
                    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)               
            .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, "1").build());          

    //Display name/Contact name                                                             
    ops.add(ContentProviderOperation                                                        
            .newInsert(ContactsContract.Data.CONTENT_URI)                                   
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,                   
                    rawContactInsertIndex)                                                  
            .withValue(ContactsContract.Data.MIMETYPE,                                      
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)      
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Jack")
            .build());                                                                      
    try {                                                                                   
        ContentProviderResult[] res = getContentResolver().applyBatch(                      
                ContactsContract.AUTHORITY, ops);                                           
    } catch (RemoteException e) {                                                           
        // TODO Auto-generated catch block                                                  
        e.printStackTrace();                                                                
    } catch (OperationApplicationException e) {                                             
        // TODO Auto-generated catch block                                                  
        e.printStackTrace();                                                                
    }                                                                                       
} 
           

现在我们来看一下运行的结果

MainActivity: readContacts: name: charles ,number: 1 234-567-8
           
运行时权限
运行时权限

从控制台打印的

log

和这两张图片我们可以看出来,我们可以成功的获取和插入通讯录。

现在我们来看看 Google Nexus 5X Android 7.0 API level 24 的设备的运行结果。

运行时权限

看一下控制台发现原来是抛出了一个异常

java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.ContactsProvider2 
requires android.permission.READ_CONTACTS or android.permission.WRITE_CONTACTS
           

这个是一行,只是我把它截取成两行,方便大家看。这边说权限被拒绝,访问ContactsProvider 需要这两个权限

android.permission.READ_CONTACTS

android.permission.WRITE_CONTACTS

。可是我明明在

AndroidManifest.xml

中注册了

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

为什么还会这样呢?我们来看看文档是怎么说的:

If the device is running Android 6.0 or higher, and your app’s target SDK is 23 or higher: The app has to list the permissions in the manifest, and it must request each dangerous permission it needs while the app is running. The user can grant or deny each permission, and the app can continue to run with limited capabilities even if the user denies a permission request.

这样看一下就明了了,在Android 6.0 以上的版本,我们除了要在

AndroidManifest.xml

中罗列出需要的权限以外还需要在运行的时候显示的申请每一个危险的权限。

我们先来看一下,在这台手机当中只有这个号码

运行时权限

现在我们运行一下程序,

在程序的第一个页面是这样的:

运行时权限

我们来看看onCreate()的方法

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

    findViewById(R.id.read).setOnClickListener(this);       
    findViewById(R.id.write).setOnClickListener(this);      
    if (checkPermission(READ_CONTACTS,CONTACTS_READ_CODE))  
        readContacts();                                     
    if (checkPermission(WRITE_CONTACTS,CONTACTS_WRITE_CODE))
        writeContacts();                                    

}                                                           
           

我们可以看到简单的这个改进,我们的程序没有crash了,在运行时跳出了权限的请求。原因在于`if`语句中的那个`checkPermission()`方法,现在我们来看看这个方法:

@TargetApi(Build.VERSION_CODES.M) //因为requestPermission()这个方法要求的api level是23,所以需要这行的annotation来标注           
public boolean checkPermission(final String permission,final int requestCode){                              
    //判断,如果SDK版本小于23就直接返回TRUE,因为在低于23的版本是不需要运行时的申请权限的                                                       
    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){                                                      
        //如果权限已经得到批准就返回TRUE                                                                                 
        if (this.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {                    
            //这个if语句必须要是第二次才会运行的,就是被DENY一次以后                                                                
            if (this.shouldShowRequestPermissionRationale(permission)) {                                    

                Log.i(TAG,                                                                                  
                        "Displaying permission rationale to provide additional context.");                  
                Snackbar.make(findViewById(R.id.activity_main),"we need to manage your contacts" ,          
                        Snackbar.LENGTH_INDEFINITE)                                                         
                        .setAction("OK", new View.OnClickListener() {                                       
                            @Override                                                                       
                            public void onClick(View view) {                                                
                                MainActivity.this.requestPermissions(new String[]{permission}, requestCode);
                            }                                                                               
                        })                                                                                  
                        .show();                                                                            

            } else {                                                                                        
                this.requestPermissions(new String[]{permission}, requestCode);                             
            }                                                                                               
            return false;                                                                                   
        }                                                                                                   
        return true;                                                                                        
    }                                                                                                       
    return true;                                                                                            
}                                                                                                                                                                                                                      
           

现在我们来看一下控制台的

log

打印情况:什么也没有。所以在这个时候,我们还访问不到

Contacts Provider

现在我们选择

DENY

选项,我们再选择

READ CONTACT

这个button,可以看到在屏幕的底部弹出了这样的一个snackbar

运行时权限
运行时权限

现在我们点

ALLOW

,可以看到在控制台打印出了这样的一条

log

MainActivity: readContacts: name: charles ,number: 1 234-567-89
           

说明我们成功的获取到了

Contact Provider

,我们再来点一下

WRITE CONTACTS

这个

button

,会发现我们不再需要申请权限了。而通讯录中也多出了一个

Jack

运行时权限

你可能有疑问,为什么点击

WRITE CONTACTS

这个

button

不需要申请权限呢?让我们接着来看看Android的文档:

If an app requests a dangerous permission listed in its manifest, and the app already has another dangerous permission in the same permission group, the system immediately grants the permission without any interaction with the user. For example, if an app had previously requested and been granted the READ_CONTACTS permission, and it then requests WRITE_CONTACTS, the system immediately grants that permission.

这段话的意思就是,只要你取得了一个dangerous group中的一个permission,那么你再申请同一个group的其他permission就可以立即被granted这个权限。

但是如果你点了

Don't ask again

这个选项,那么你后面的申请都是无效的了。

其他注意事项:

如果你使用运行时权限,你还需要实现这个接口

ActivityCompat.OnRequestPermissionsResultCallback

,然后实现它的方法

@Override                                                                                                            
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == CONTACTS_READ_CODE)                                                                           
            if (grantResults.length >  && grantResults[]==PackageManager.PERMISSION_GRANTED)                           
                 readContacts();                                                                                          
        if (grantResults.length >  && requestCode == CONTACTS_WRITE_CODE)                                               
             if (grantResults[] == PackageManager.PERMISSION_GRANTED)                                                    
                 writeContacts();                                                                                         
}                                                                                                                    
           

因为在

checkPermission()

中的

requestPermission()

会去回调

ActivityCompat.OnRequestPermissionsResultCallback

的方法。