---
title: 'Android 權限'
disqus: kyleAlien
---
Android 權限
===
## OverView of Content
[**官方文檔**](https://developer.android.com/guide/topics/permissions/overview) 有詳細說明要如何控制 Android 的權限 (在 API 23 以後特別需要)
[TOC]
## Manifest 權限聲明
應用所需的**所有的權限都 (不論是普通權限 or 危險權限) 必須提前在 Manifest 中聲明**,若是沒有聲明,可是連用都完全不能使用,也又不用下面這些步驟了
```xml=
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.snazzyapp">
<!-- 權限聲明 -->
<uses-permission android:name="android.permission.SEND_SMS"/>
<application ...>
...
</application>
</manifest>
```
### 權限比對
* 在 targetSdkVersion >= 23 時 (Gralde),用戶在安裝時是不會看到任何的權限通知,**必須要在用戶使用應用時才去申請權限**
```groovy=
android {
compileSdkVersion 30
buildToolsVersion "30.0.0"
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 21
// 比對設定,若超過 23 則運行時申請
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
```
:::info
* Android 5.1.1 (API 22) <= 系統,則會在 Google Play 安裝時就會要求權限
> 
:::
* 不能指望每次都擁有需要的權限 (你得每次問 :cry:),**若是沒有得到權限就訊行則會拋出 SecurityException**
### 硬體權限
* 由於並不是每台 Android 手機都有你目前應用所需的硬體功能 (像有一些可能缺少了羅盤、NFC 功能),那你的應用就不能使用了
```xml=
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.snazzyapp">
<!-- 硬體權限聲明 -->
<!-- 若 required 是 ture 則不能安裝在該裝置上,若為 false 則代表可以安裝在該裝置上 -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application ...>
...
</application>
</manifest>
```
* 若 `android:required="false"` 代表就算該手機並沒有這個硬體裝置,仍可安裝這個應用,這時可以**改為使用 PackageManager#hasSystemFeature 確認設備是否有該功能**
```java=
public boolean checkHardware() {
return PackageManager.hasSystemFeature(...);
}
```
:::success
* 若是未在 `<uses-feature>` 中設定,當 Google Play 發現應用請求後會假定你的應用 **必須** 要該權限,相當於幫你設定了 `android:required="true"`
:::
## 保護級別
| 級別 | 說明 |
| -------- | -------- |
| 普通權限 | 系統不會向用戶提示,用戶也無法取消。這種權限將會直接授予應用,但仍需再 Manifest 中添加權限 |
| 簽名權限 | 在安裝時期授予應用權限 |
| 危險權限 | 涉及用戶隱私 (對用戶儲存數據),必須在運行時提示用戶授予該應用權限 |
| 特殊權限 | **^1^ SYSTEM_ALERT_WINDOW、^2^ WRITE_SETTINGS** 特別敏感 (對系統操作) |
### 權限組
* **對權限的要求會有分組**,避免不必要的多次詢問,例如:READ_SMS 被授予的權限,WRITE_SMS 也會 **自動** 被授予權限
> 
* 在 Android 6.0 (API 23) & 用戶的 targetSdkVersion >= 23,當該應用使用到 **危險權限** 時,會跳出權限詢問框,**Dialog 內容就是 ==權限組==,而不是詳細內容**
:::danger
* Android 權限組判斷 ?
將來 Android SDK 可能將特定權限移動到另外一個權限組,這些權限組是有可能根據 SDK 改變而改變,**所以==不要依照權限組來判斷權限設置== (不要假設權限組相同)**
例如:Android 8.1 起,將 READ_CONTACTS & WRITE_CONTACTS 放置在同一組,在這之前的版本就必須分開判斷
:::
## 權限請求
有關於權限的請求,請先了解 **保護級別**
| API | 功能 | 注意 |
| -------- | -------- | -------- |
| checkSelfPermission | 檢查自身權限,返回 PackageManager 兩種情況,^1^PERMISSION_GRANTED(成功),^2^PERMISSION_DENIED(失敗) | **在 API 23 以上** |
| requestPermissions | 權限要求,可同時要求多種權限 | 同上運行在 23 以上,並且**有一個自訂的 requestCode(可用來確認請求)** |
| onRequestPermissionsResult(覆寫方法) | 當你使用 requestPermissions 後,他會將用戶的結果返回到該函數中 | 它的參數對應了 requestPermissions 參數|
| **shouldShowRequestPermissionRationale** | 這個功能較特別,**它可以判斷是否讓用戶進入權限說明**(自己製作) | **^1^用戶不曾拒絕返回 false**,**^2^用戶拒絕之後就返回 ture**,**^3^用戶拒絕並不再同意返回 false** |
### 檢查權限
* 如果該應用需要一個 **危險權限**,**在 Android 6.0 (API 23) 以後用戶可以隨時撤銷該應用的權限授予**
:::info
建議**在用戶使用的功能有需求時再檢查 & 請求權限**,不要在應用開始時就要求權限
:::
* **使用 ==ContextCompat== 類的 ++checkSelfPermission++ 方法來檢查權限的授予狀態**,第一個參數為你所需要求的權限(可為多個),可把第二個參數 requestCode 就是這次請求的 ID
```java=
/**
* 權限檢查函數 checkSelf
* 假設檢查為寫入儲存 WRITE_EXTERNAL_STORAGE
*/
private static final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
private void checkSelf() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String msg;
if(checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
msg = "Have permission";
} else {
msg = "Request permission";
requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
showToast(msg);
}
}
private void showToast(String str) {
if(str == null) return;
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}
```
### 檢查用戶回應
* 這裡必須覆寫 onRequestPermissionsResult 方法,並使用它給予的參數做判斷,**^1^requestCode 對應請求時所填寫的請求 ID,^2^這次請求的權限,^3^對應權限數組返回使用者是否同意**
* 以下會配合 shouldShowRequestPermissionRationale 進入請求教學 (或是說,解釋為何需要該權限),這個函數有三種狀況
| 狀況 | 選擇圖示 | 函數返回結果 |
| -------- | -------- | -------- |
| 用戶不曾拒絕 |  | false |
| 用戶拒絕之後就 |  | ture |
| 用戶拒絕 ++**並不再同意**++ |  | false |
```java=
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == 1) {
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 成功
showToast("Open Permission success");
} else {
String msg = "Open Permission fail";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(shouldShowRequestPermissionRationale(permission)) { // 第一次拒絕後就會觸發 true,若是以確認不再同意則為 false
// 解釋權限 (教學)
msg = "已拒絕一次";
showTeachDialog();
}
}
showToast(msg);
}
}
}
// 解釋 Dialog
public void showTeachDialog() {
AlertDialog alertDialog = new AlertDialog.Builder(this)
.setMessage(permission + "to recode something")
.setTitle("Why use it")
.setNegativeButton("Ok", (dialog, which) -> {
// 要求權限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
dialog.dismiss();
})
.setNeutralButton("Think", (dialog, which) -> {
dialog.dismiss();
})
.setPositiveButton("Leave", (dialog, which) -> {
finish();
})
.create();
alertDialog.show();
}
// Toast
private void showToast(String str) {
if(str == null) return;
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}
```
**--教學畫面--**
> 
## Appendix & FAQ
:::info
:::
###### tags: `Android 基礎`