天天看點

運作時權限

在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

的方法。