在 2015 年 10 月釋出了一個令開發者震驚令使用者振奮的新規則:
Android 6.0 Marshmallow「權限索取」

當開發者想要透過 App 和使用者有互動行為時(例如拿照片、打電話 …等),
都必須「事先詢問」使用者是否允許 App 做這些事情。

你可能會問,難道 6.0 以前都沒有「權限」的概念嗎?
不,其實是有的。
只是授與的時間點,而是在安裝 App 的當下。

這對開發者與使用者來說,都不是一個太好的體驗。
因此,Google 決定調整這老舊的流程,將時間點改為「即時 (Runtime) 詢問」
也就是需要使用到權限時,才跳出授與視窗,詢問使用者。

重新仔細說明一下「權限」的意思。

當任一個應用程式(例如 Facebook)想要取得你的照片、通訊錄,甚至是所在位置
理當都必須要詢問過使用者要不要給予
畢竟這些資料都是相當隱私且必須被保護的

關於這方面 iOS 一直都做得很有規矩
在使用的當下便會跳出小視窗來詢問使用者是否要提供

回頭來看 Android
2009年 開始慢慢普及社會後到現在
Android 並沒有主動去做這種事情
而是在使用者安裝 App 的同時將所有的權限都一次搞定

有經歷過 Android 6.0 以前的使用者應該不陌生上面的畫面,
除了使用者很少會詳細閱讀外,這種方式都是一次將「所有權限」的資格都取得,
使用者只要有其中一項不願意授與,別無他法,就只能放棄安裝 App。

直到 2015年 10月 Google 開始正視了這件事情
於是拿新釋出的 OS - 6.0 Marshmallow 開刀
決定好好管制這失控的安全

不鳴則已一鳴驚人
除了可以允許、拒絕外,還可以永久拒絕
這也意味著開發者必須花點小功夫處理
如果你想要忽略這件事
當你打算偷偷使用權限時
應用程式會直接閃退給你看(錯誤訊息為 Permission denial.)

進入重點。

首先,先解釋整個權限索取的流程
當然目前也有許多第三方 library 可以用,方便得很

倘若時間不趕
建議你還是搞懂會比較好

危險權限

權限百百種
Android 並不會殘忍的要你將所有權限都做處理

官網列出了他們認為的 危險權限
並將同類型的權限視為同一群體 (Group)
因此同類權限只要取一次就可以通用全部

例如:

READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS

皆被歸類為 CONTACTS 權限

詳細的資訊可以在官網查詢得到

實作

接下來,我以「位置(location)」權限來示範:

(1) 將所需權限定義在 AndroidManifest 裡

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
        -> 網路定位
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        -> GPS

(2) 判斷權限是否已取得(只要在 ACCESS_COARSE_LOCATION 與 ACCESS_FINE_LOCATION 擇一來處理就好)

    private boolean isLocationPermissionGranted() {
        return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
    }

(3) 取得權限(只要在 ACCESS_COARSE_LOCATION 與 ACCESS_FINE_LOCATION 擇一來處理就好)

    private void requestLocationPermission() {
        ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, AppConstant.LOCATION_REQUEST_CODE);
        // AppConstant.LOCATION_REQUEST_CODE 為自己隨意定義的 int(例如:999)
    }

結果截圖如下:

這是第一次要求權限時的畫面
使用者有兩種選擇

  1. 接受
  2. 拒絕

(4) 偵測使用者是否按下「拒絕」
當使用者按下拒絕,開發者當然不能悶不吭聲就讓使用者流走
於是我建議您在偵測到使用者按下拒絕後
可以跳出提示來向使用者說明一下您要這個權限的原因
降低他們的防備心

首先,你要跳出提示視窗的 Activity 裡(剛剛在 step3 時帶入的 activity)
覆寫 onRequestPermissionsResult 這個 method
這 method 你可以把它想像是 requestPermissions 的 callback
在使用者對視窗做動作時,這個 method 將會被呼叫
並帶回使用者的動作讓你判斷接下來該做什麼

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == AppConstant.LOCATION_REQUEST_CODE) {
        // 因為這個 method 會帶回任何權限視窗的結果,所以我們要用 requestCode 來判斷這是哪一個權限

            if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
            // 不等於 PERMISSION_GRANTED 代表被按下拒絕

                Toast...show;
                // 跳出否些提示 ...(例如:這權限是為了讓你更快找到 xxxx)
            }
        }
    }

第一階段告一段落。

看似圓滿的結局 其實還有另一種狀況
也就是在使用者第二次按下「拒絕」後
跳出的權限視窗長得與第一次並不一樣:

看得出來哪裡不一樣嗎?

沒錯,Android 提供給使用者「永遠不要再問我」的權力
(因為已經按過拒絕代表這使用者已經不耐煩一次了)

這時候就比較麻煩了
當使用者選擇這選項後,權限視窗將不再出現
就算你呼喚了一百次 requestPermissions 它一樣不會理你

這時候我們唯一的解決辦法
就是將使用者導向系統的「應用程式設定」頁面
因為使用者必須手動在此打開權限才行

不過在把使用者導過去之前
我們必須先偵測到使用者真的做了這殘忍的決定

Android 提供了一個 method shouldShowRequestPermissionRationale
會回傳 true / false

true = 請給使用者更多的說明
false = 不需再給使用者說明

什麼時候會 return true 呢?
第 n 次按下拒絕

什麼時候會 return false 呢?
a) 已接受
b) 已勾選再也不要問我 -> 因為勾選了代表使用者就是不想給,也就代表你也不需要再給更多說明

綜合以上,我們可以理出
onRequestPermissionsResult 收到
a) grantResults[0] != PackageManager.PERMISSION_GRANTED
b) shouldShowRequestPermissionRationale == false
以上兩個條件成立時
便是代表使用者勾選了「不要再提醒我」

(5) 偵測使用者是否勾選「不要再問我」選項

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == AppConstant.LOCATION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)) {
                    openAppSettingsIntent();
                }
            }
        }
    }

step6. 前往系統設定頁面

    private void openAppSettingsIntent() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getPackageName(), null));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);
    }

到此權限流程都走了一遍。

很可惜的是目前導向系統應用程式設定頁面後我們什麼也不能做,
所以建議在將使用者導過去之前,可以先說明權限理由,或是教導怎麼打開,
才不會使用者一頭霧水(畢竟不是人人都懂這流程)。

其實目前還是有很多人沒有處理 permission 問題。
閃退的閃退,繞過的繞過。

理解原理後其實真的不難,能不透過第三方套件就不要,這樣才不會發生問題時很難處理。