一、概述
在Android6.0之前的系統中,APP只要在AndroidManifest.xml聲明了權限,就獲得了授權,用戶只能選擇授權或者不安裝該應用。Android 6.0在原有的AndroidManifest.xml聲明權限的基礎上,又新增了運行時權限動態檢測,使用:日歷、攝像頭、通訊錄、地理位置、麥克風、電話、短信、存儲空間、身體傳感器等權限都需要在運行時判斷,使用其他權還是跟原來一樣,只要在AndroidManifest.xml聲明即獲得授權。對於未獲得相應權限授權的操作,將返回空或者拋出異常,如果APP中沒有對此作出良好的處理,有可能產生崩潰現象。因此,在Android 6.0中開發APP,我們需要編寫代碼適應這種機制。
二、運行時權限管理
Android系統中的權限被分為以下幾類:
1)normal:表示權限是低風險的,不會對系統、用戶或其他應用程序造成危害,應用直接授予此權限;
2)dangerous:表示權限是高風險的,系統將可能要求用戶輸入相關信息,才會授予此權限;
3)signature:表示只有當應用程序所用數字簽名與聲明引權限的應用程序所用數字簽名相同時,才能將權限授給它;
4)signatureOrSystem: 表示將權限授給具有相同數字簽名的應用程序或system分區的應用,已廢棄,使用privileged代替。
5)privileged:同signatureOrSystem。
在Android 6.0以前的系統中,“dangerous”與“normal”等級的權限是沒有區別的,只要在AndroidManifest.xml聲明了權限,就授予授權。在Android 6.0及以后系統中,25個“dangerous”等級的權限被分為9組,需要由應用程序自己去請求授權,不請求的話默認是“禁止”,同一組的任何一個權限被授權了,其他權限也自動被授權。
Permission Group | Permissions |
---|---|
android.permission-group.CALENDAR |
|
android.permission-group.CAMERA |
|
android.permission-group.CONTACTS |
|
android.permission-group.LOCATION |
|
android.permission-group.MICROPHONE |
|
android.permission-group.PHONE |
|
android.permission-group.SENSORS |
|
android.permission-group.SMS |
|
android.permission-group.STORAGE |
|
權限請求會觸發系統彈出權限請求對話框。如果是第一次請求該權限,則會有權限請求對話框彈出來,如下圖所示:
如果第一次用戶選擇了“拒絕”,那么第二次請求權限彈出的權限請求對話框彈出來會包含一個“不再詢問”勾選框,如下圖所示:
如果用戶選擇了“允許”,無論是否勾選”記住“,以后每次權限請求都會允許,不會再彈出權限請求對話框(實測結果是這樣,貌似此處Google邏輯有點問題)。如果用戶沒有勾選“記住”,且用戶選擇“拒絕”,那么每次權限請求都會觸發上圖的彈出權限請求對話框,除非用戶選擇“允許”或者勾選“記住”。
用戶可以進入“設置”-“應用”中去修改應用的權限授權狀態,在此處修改后,應用程序權限請求時將不再彈出權限請求對話框。
由於目前絕大部分應用還不支持這種特性,因此Google做了妥協,targetSdkVersion<23時,采用以前的權限管理方式,AndroidManifest.xml中聲明的權限還是安裝后全部授權,當APP的targetSdkVersion大於等於23時,APP中的一些權限將需要APP自己添加代碼申請權限。
三、相關API
Android 6.0 中增加了一些API用於運行時權限的申請與授權狀態查詢:
1)Context
Context中提供了查詢當前應用權限授權狀態的API——checkSelfPermission, 當其返回0,表示已授權(PackageManager.PERMISSION_GRANTED);返回1,表示未授權(PackageManager.PERMISSION_DENIED):
public int checkSelfPermission(String permission) { if (permission == null) { throw new IllegalArgumentException("permission is null"); } return checkPermission(permission, Process.myPid(), Process.myUid()); }
2) Activity
在Activity中提供了判斷是否應該說明申請權限理由的API——shouldShowRequestPermissionRationale,該函數返回值如下:
a. 當應用第一次請求權限時返回false;
b. 第一次請求權限時,用戶拒絕了,下一次返回 true,這時可以給用戶說明需要改權限的理由;
c. 第二次請求權限時,用戶拒絕了,並選擇了“不再提醒”的選項時,返回false。以后權限申請對話框是不會彈出的,權限請求都會被拒絕。
d. 在請求權限時,用戶允許了,下一次返回false;
e. 第二次以后請求權限時,用戶拒絕了,並沒有選擇“不再提醒”的選項時,返回true,這是也可以給用戶說明需要改權限的理由;
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) { return getPackageManager().shouldShowRequestPermissionRationale(permission); }
另外提供了申請權限的API——requestPermissions,它實質上是一個startActivityForResult,里面可以請求多個權限,會啟動一個系統Activity來判斷該應用的權限狀態。沒有永久禁止或者允許的話會彈出權限請求對話框
public final void requestPermissions(@NonNull String[] permissions, int requestCode) { if (mHasCurrentPermissionsRequest) { Log.w(TAG, "Can reqeust only one set of permissions at a time"); // Dispatch the callback with empty arrays which means a cancellation. onRequestPermissionsResult(requestCode, new String[0], new int[0]); return; } Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions); startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null); mHasCurrentPermissionsRequest = true; }
requestPermissions返回的結果在onRequestPermissionsResult,傳入參數包含權限是否授權的信息。
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { /* callback - no nothing */ }
3)Fragement
Fragement 中提供的API與Activity類似,也提供shouldShowRequestPermissionRationale、requestPermissions、onRequestPermissionsResult等方法。
為了兼容舊版本的Android系統,Google在support-v4庫中添加了類似的接口:
ContextCompat.checkSelfPermission()
ActivityCompat.requestPermissions()
ActivityCompat.OnRequestPermissionsResultCallback
ActivityCompat.shouldShowRequestPermissionRationale()
FragmentCompat.requestPermissions()
FragmentCompat.OnRequestPermissionsResultCallback
FragmentCompat.shouldShowRequestPermissionRationale()
四、應用適配
當應用的targetSdkVersion》=23時,應用中需要使用“dangerous”等級的權限時,都需要進行權限授權狀態判斷、申請權限、處理權限申請結果。可以按照以下流程進行:
如果APP是作為第三方APP,建議使用support-v4庫提供的接口。
1)檢測權限,例如:
int permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
2)請求權限,例如
// Check if the Camera permission has been granted if (permission== PackageManager.PERMISSION_GRANTED) { // Permission is already available, start camera preview Snackbar.make(mLayout, "Camera permission is available. Starting preview.", Snackbar.LENGTH_SHORT).show(); startCamera(); } else { // Permission is missing and must be requested. requestCameraPermission(); } private void requestCameraPermission() { // Permission has not been granted and must be requested. if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { // Provide an additional rationale to the user if the permission was not granted // and the user would benefit from additional context for the use of the permission. // Display a SnackBar with a button to request the missing permission. Snackbar.make(mLayout, "Camera access is required to display the camera preview.", Snackbar.LENGTH_INDEFINITE).setAction("OK", new View.OnClickListener() { @Override public void onClick(View view) { // Request the permission ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA); } }).show(); } else { Snackbar.make(mLayout, "Permission is not available. Requesting camera permission.", Snackbar.LENGTH_SHORT).show(); // Request the permission. The result will be received in onRequestPermissionResult(). ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA); }
請求權限時可以一次性申請多個權限。
3)處理權限請求結果
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { // BEGIN_INCLUDE(onRequestPermissionsResult) if (requestCode == PERMISSION_REQUEST_CAMERA) { // Request for camera permission. if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission has been granted. Start camera preview Activity. Snackbar.make(mLayout, "Camera permission was granted. Starting preview.", Snackbar.LENGTH_SHORT) .show(); startCamera(); } else { // Permission request was denied. Snackbar.make(mLayout, "Camera permission request was denied.", Snackbar.LENGTH_SHORT) .show(); } } }
測試代碼:
package com.example.joy.granttest; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private EditText mEtNumber; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEtNumber = (EditText) findViewById(R.id.et_number); } public void onDialClick(View v) { switch (v.getId()) { case R.id.btn_dial: if (mEtNumber.getText() == null) { Toast.makeText(MainActivity.this, "number is null !", Toast.LENGTH_SHORT).show(); return; } if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CALL_PHONE)) { } ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 200); } else { callPhone(); } break; default: break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == 200) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { callPhone(); } else { Toast.makeText(MainActivity.this, "Pemission Denied!", Toast.LENGTH_SHORT).show(); } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } private void callPhone() { String phoneNumber = mEtNumber.getText().toString(); Intent intent = new Intent(Intent.ACTION_CALL); Uri uri = Uri.parse("tel:" + phoneNumber); intent.setData(uri); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling // ActivityCompat#requestPermissions // here to request the missing permissions, and then overriding // public void onRequestPermissionsResult(int requestCode, String[] permissions, // int[] grantResults) // to handle the case where the user grants the permission. See the documentation // for ActivityCompat#requestPermissions for more details. return; } startActivity(intent); } }