Android新特性解析一:執行時許可權

weixin_33670713發表於2016-11-29

Android新特性解析一:執行時許可權

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章資源

在討論執行時許可權以前,我們先來回憶一下以前的許可權使用,通常我們申請一個許可權,必須在應用manifest檔案中包含一個或多個 <uses-permission> 標記。

例如,需要監控傳入的簡訊的應用要指定:

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

Android 6.0開始引入了新的執行時許可權檢查授權機制,替代了之前安裝應用的時候對許可權進行授權的方案。該方案將許可權分為正常許可權和危險許可權,對於
正常許可權我們想以前一樣在manifest裡註冊即可,但是對於危險許可權即便我們註冊了,它還是會在執行時進行許可權檢查,這便是執行時許可權。

具體說來:

  • 如果裝置執行的是 Android 6.0(API 級別 23)或更高版本,並且應用的 targetSdkVersion 是 23 或更高版本,則應用在執行時向使用者請求許可權。用
    戶可隨時呼叫許可權,因此應用在每次執行時均需檢查自身是否具備所需的許可權。如需瞭解在應用中請求許可權的更多資訊,請參閱使用系統許可權培訓指南。
  • 如果裝置執行的是 Android 5.1(API 級別 22)或更低版本,並且應用的 targetSdkVersion 是 22 或更低版本,則系統會在使用者安裝應用時要求使用者
    授予許可權。如果將新許可權新增到更新的應用版本,系統會在使用者更新應用時要求授予該許可權。使用者一旦安裝應用,他們撤銷許可權的唯一方式是解除安裝應用。通常,
    許可權失效會導致 SecurityException 被扔回應用。但不能保證每個地方都是這樣。例如,sendBroadcast(Intent) 方法在資料傳遞到每個接收者時會檢查
    許可權,在方法呼叫返回後,即使許可權失效,您也不會收到異常。但在幾乎所有情況下,許可權失效會記入系統日誌。

許可權型別

系統許可權分為兩類,正常許可權和危險許可權:

  • 正常許可權涵蓋應用需要訪問其沙盒外部資料或資源,但對使用者隱私或其他應用操作風險很小的區域。例如,設定時區的許可權就是正常許可權。如果應用宣告其需要
    正常許可權,系統會自動向應用授予該許可權。
  • 危險許可權涵蓋應用需要涉及使用者隱私資訊的資料或資源,或者可能對使用者儲存的資料或其他應用的操作產生影響的區域。例如,能夠讀取使用者的聯絡人屬於危險
    許可權。如果應用宣告其需要危險許可權,則使用者必須明確嚮應用授予該許可權。

正常許可權

  • 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

參考連結
https://developer.android.com/guide/topics/security/normal-permissions.html

危險許可權

危險許可權是分組的,同一組的任何一個許可權被授權了,其他許可權也自動被授權。例如,一旦WRITE_CONTACTS被授權了,app也有WRITE_CONTACTS
和GET_ACCOUNTS許可權了。

group:android.permission-group.CONTACTS

  • permission:android.permission.WRITE_CONTACTS
  • permission:android.permission.GET_ACCOUNTS
  • permission:android.permission.WRITE_CONTACTS

group:android.permission-group.PHONE

  • permission:android.permission.READ_CALL_LOG
  • permission:android.permission.READ_PHONE_STATE
  • permission:android.permission.CALL_PHONE
  • permission:android.permission.WRITE_CALL_LOG
  • permission:android.permission.USE_SIP
  • permission:android.permission.PROCESS_OUTGOING_CALLS
  • permission:com.android.voicemail.permission.ADD_VOICEMAIL

group:android.permission-group.CALENDAR

  • permission:android.permission.READ_CALENDAR
  • permission:android.permission.WRITE_CALENDAR

group:android.permission-group.CAMERA

  • permission:android.permission.CAMERA

group:android.permission-group.SENSORS

  • permission:android.permission.BODY_SENSORS

group:android.permission-group.LOCATION

  • permission:android.permission.ACCESS_FINE_LOCATION
  • permission:android.permission.ACCESS_COARSE_LOCATION

group:android.permission-group.STORAGE

  • permission:android.permission.READ_EXTERNAL_STORAGE
  • permission:android.permission.WRITE_EXTERNAL_STORAGE

group:android.permission-group.MICROPHONE

  • permission:android.permission.RECORD_AUDIO

group:android.permission-group.SMS

  • permission:android.permission.READ_SMS
  • permission:android.permission.RECEIVE_WAP_PUSH
  • permission:android.permission.RECEIVE_MMS
  • permission:android.permission.RECEIVE_SMS
  • permission:android.permission.SEND_SMS
  • permission:android.permission.READ_CELL_BROADCASTS

我們可以使用 adb 工具從命令列管理許可權:

按組列出許可權和狀態:

$ adb shell pm list permissions -d -g

授予或撤銷一項或多項許可權:

$ adb shell pm [grant|revoke] <permission-name> ...

參考連結
https://developer.android.com/guide/topics/security/permissions.html#permissions

執行時許可權處理流程

1 向清單新增許可權

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.snazzyapp">

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

    <application ...>
        ...
    </application>

</manifest>

2 檢查許可權

如果我們的應用需要危險許可權,則每次執行需要這一許可權的操作時您都必須檢查自己是否具有該許可權。使用者始終可以自由呼叫此許可權,因此,即使應用昨天使用了相機,它
不能假設自己今天仍具有該許可權。要檢查是否具有某項許可權,可以呼叫 ContextCompat.checkSelfPermission() 方法。

例如,以下程式碼段顯示瞭如何檢查 Activity 是否具有寫入聯絡人的許可權:

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

如果應用具有此許可權,方法將返回 PackageManager.PERMISSION_GRANTED,並且應用可以繼續操作。如果應用不具有此許可權,方法將返回 PERMISSION_DENIED,且應用必須明確向使用者要求許可權。

3 請求許可權

//請求許可權
ActivityCompat.requestPermissions(PermissionActivity.this,
        new String[]{Manifest.permission.WRITE_CONTACTS},
        REQUEST_CODE_FOR_PERMISSION_CALLBACK);

該方法會彈出一個系統對話方塊,來供使用者選擇是否允許該許可權申請,當然使用者可能會拒絕我們的許可權申請,這種情況下說明使用者不理解我們
為什麼要申請這個許可權,這個時候最好的做法是給使用者一個解釋,如下所示:

//是否給使用者一個關於許可權申請的解釋
if (ActivityCompat.shouldShowRequestPermissionRationale(PermissionActivity.this,
        Manifest.permission.WRITE_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 {
    //請求許可權
    ActivityCompat.requestPermissions(PermissionActivity.this,
            new String[]{Manifest.permission.WRITE_CONTACTS},
            REQUEST_CODE_FOR_PERMISSION_CALLBACK);
}

注:如果應用之前請求過此許可權但使用者拒絕了請求,此方法將返回 true。如果使用者在過去拒絕了許可權請求,並在許可權請求系統對話方塊中選擇了 Don't ask again 選
項,此方法將返回 false。如果裝置規範禁止應用具有該許可權,此方法也會返回 false。此方法非同步執行:它會立即返回,並且在使用者響應對話方塊之後,系統會使用結
果呼叫應用的回撥方法,將應用傳遞的相同請求程式碼傳遞到 requestPermissions()。

4 處理許可權請求響應

當應用請求許可權時,系統將向使用者顯示一個對話方塊。當使用者響應時,系統將呼叫應用的 onRequestPermissionsResult() 方法,向其傳遞使用者響應。我們可以在此方法裡是否已獲得相應許可權。
回撥會將您傳遞的相同請求程式碼傳遞給 requestPermissions()。

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case REQUEST_CODE_FOR_PERMISSION_CALLBACK:
            //如果許可權請求被拒絕,則grantResults為空
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //許可權已經被授予,可以新增聯絡人了
                Toast.makeText(PermissionActivity.this, "新增聯絡人許可權被授予", Toast.LENGTH_LONG).show();
                insertDummyContact();
            } else {
                //許可權請求被拒絕
                Toast.makeText(PermissionActivity.this, "新增聯絡人許可權被拒絕", Toast.LENGTH_LONG).show();
            }
            break;
    }
}

整個流程的程式碼如下:


/******************
 * 原生程式碼申請許可權 *
 ******************/
 
private static String DUMMY_CONTACT_NAME = "__DUMMY CONTACT from runtime permissions sample";
private static final int REQUEST_CODE_FOR_PERMISSION_CALLBACK = 0x000001;

private void requestRuntimePermission() {
    // API 23及其以後的版本
    if (Build.VERSION.SDK_INT >= 23) {
        // 檢測是否已經被授權
        if (ContextCompat.checkSelfPermission(PermissionActivity.this,
                Manifest.permission.WRITE_CONTACTS)
                != PackageManager.PERMISSION_GRANTED) {
            //是否給使用者一個關於許可權申請的解釋
            if (ActivityCompat.shouldShowRequestPermissionRationale(PermissionActivity.this,
                    Manifest.permission.WRITE_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 {
                //請求許可權
                ActivityCompat.requestPermissions(PermissionActivity.this,
                        new String[]{Manifest.permission.WRITE_CONTACTS},
                        REQUEST_CODE_FOR_PERMISSION_CALLBACK);
            }
        }
    }
    //API 23以前版本
    else {

    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case REQUEST_CODE_FOR_PERMISSION_CALLBACK:
            //如果許可權請求被拒絕,則grantResults為空
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //許可權已經被授予,可以新增聯絡人了
                Toast.makeText(PermissionActivity.this, "新增聯絡人許可權被授予", Toast.LENGTH_LONG).show();
                insertDummyContact();
            } else {
                //許可權請求被拒絕
                Toast.makeText(PermissionActivity.this, "新增聯絡人許可權被拒絕", Toast.LENGTH_LONG).show();
            }
            break;
    }
}

/**
 * Accesses the Contacts content provider directly to insert a new contact.
 * <p>
 * The contact is called "__DUMMY ENTRY" and only contains a name.
 */
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);

    // First, set up a new raw contact.
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
    operations.add(op.build());

    // Next, set the name for the contact.
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
                    DUMMY_CONTACT_NAME);
    operations.add(op.build());

    // Apply the operations.
    ContentResolver resolver = getContentResolver();
    try {
        resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    } catch (RemoteException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    } catch (OperationApplicationException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    }
}

簡化許可權處理流程

如果每次處理執行時許可權都要寫辣麼一堆程式碼,估計我們也要被累死了~~,所以也用相應的開源庫來簡化執行時許可權的處理。試用了很多,目前感覺最好用
的是PermissionsDispatcher,該庫試用使用註解的方式,動態生成類處理執行時許可權,下面介紹一個它的試用流程。

PermissionsDispatcher

1 安裝外掛

AndroidStudio安裝外掛PermissionsDispatcher

2 新增依賴

注:當前的${latest.version}是2.2.0

For Android Gradle Plugin >= 2.2 users

To add it to your project, include the following in your app module build.gradle file:

dependencies {
  compile 'com.github.hotchemi:permissionsdispatcher:${latest.version}'
  annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:${latest.version}'
}

For Android Gradle Plugin < 2.2 users

To add it to your project, include the following in your project build.gradle file:

buildscript {
  dependencies {
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

And on your app module build.gradle:

apply plugin: 'android-apt'

dependencies {
  compile 'com.github.hotchemi:permissionsdispatcher:${latest.version}'
  apt 'com.github.hotchemi:permissionsdispatcher-processor:${latest.version}'
}

3 右鍵點選生成執行時許可權程式碼

填寫好方法名後生成的方法會帶有以下4個註解:

Annotation Required Description
@RuntimePermissions Register an Activity or Fragment to handle permissions
@NeedsPermission Annotate a method which performs the action that requires one or more permissions
@OnShowRationale Annotate a method which explains why the permission/s is/are needed. It passes in a PermissionRequest object which can be used to continue or abort the current permission request upon user input
@OnPermissionDenied Annotate a method which is invoked if the user doesn't grant the permissions
@OnNeverAskAgain Annotate a method which is invoked if the user chose to have the device "never ask again" about a permission

/******************
 * 註解方式申請許可權 *
 ******************/

private static String DUMMY_CONTACT_NAME = "__DUMMY CONTACT from runtime permissions sample";

private void setupView() {
    findViewById(R.id.btn_request_runtime_permission).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //insertContactWithCheck()是自動生成的方法
            PermissionActivityPermissionsDispatcher.insertContactWithCheck(PermissionActivity.this);
        }
    });
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    //許可權申請的結果交由PermissionActivityPermissionsDispatcher來處理
    PermissionActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
}

/**
 * 需要申請許可權的操作
 */
@NeedsPermission(Manifest.permission.WRITE_CONTACTS)
void insertContact() {
    insertDummyContact();
}

/**
 * 解釋許可權申請原因
 *
 * @param request request
 */
@OnShowRationale(Manifest.permission.WRITE_CONTACTS)
void onShowRationale(final PermissionRequest request) {
    new AlertDialog.Builder(this)
            .setMessage("解釋為何申請許可權")
            .setPositiveButton("同意", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    request.proceed();
                }
            })
            .setNegativeButton("拒絕", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    request.cancel();
                }
            })
            .show();
}

/**
 * 許可權申請拒絕
 */
@OnPermissionDenied(Manifest.permission.WRITE_CONTACTS)
void onPermissionDenied() {
    Toast.makeText(PermissionActivity.this, "新增聯絡人許可權被拒絕", Toast.LENGTH_LONG).show();
}

/**
 * 許可權申請不再詢問
 */
@OnNeverAskAgain(Manifest.permission.WRITE_CONTACTS)
void onNeverAskAgain() {
    Toast.makeText(PermissionActivity.this, "新增聯絡人許可權不再被詢問", Toast.LENGTH_LONG).show();
}

/**
 * Accesses the Contacts content provider directly to insert a new contact.
 * <p>
 * The contact is called "__DUMMY ENTRY" and only contains a name.
 */
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);

    // First, set up a new raw contact.
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
    operations.add(op.build());

    // Next, set the name for the contact.
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
                    DUMMY_CONTACT_NAME);
    operations.add(op.build());

    // Apply the operations.
    ContentResolver resolver = getContentResolver();
    try {
        resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    } catch (RemoteException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    } catch (OperationApplicationException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    }
}

相關文章