為什麼說在 Android 中請求許可權從來都不是一件簡單的事情?

daxuesheng發表於2021-09-09

週末時間參加了東莞和深圳的兩場 GDG,因為都是線上參與,所以時間上並不趕,我只需要坐在家裡等活動開始就行了。

等待的時間一時興起,突然想寫一篇原創,聊一聊我自己在寫 Android 許可權請求程式碼時的一些技術心得。

正如這篇文章標題所描述的一樣,在 Android 中請求許可權從來都不是一件簡單的事情。為什麼?我認為 Google 在設計執行時許可權這塊功能時,充分考慮了使用者的使用體驗,但是卻沒能充分考慮開發者的編碼體驗。

之前在公眾號的留言區和大家討論時,有朋友說:我覺得 Android 提供的執行時許可權 API 很好用呀,並沒有覺得哪裡使用起來麻煩。

真的是這樣嗎?我們來看一個具體的例子。

假設我正在開發一個拍照功能,拍照功能通常都需要用到相機許可權和定位許可權,也就是說,這兩個許可權是我實現拍照功能的先決條件,一定要使用者同意了這兩個許可權我才能繼續進行拍照。

那麼怎樣去申請這兩個許可權呢?Android 提供的執行時許可權 API 相信每個人都很熟悉了,我們自然而然可以寫出如下程式碼:

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        ActivityCompat.requestPermissions(this,

            arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1)

    }


    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {

        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {

            1 -> {

                var allGranted = true

                for (result in grantResults) {

                    if (result != PackageManager.PERMISSION_GRANTED) {

                        allGranted = false

                    }

                }

                if (allGranted) {

                    takePicture()

                } else {

                    Toast.makeText(this, "您拒絕了某項許可權,無法進行拍照", Toast.LENGTH_SHORT).show()

                }

            }

        }

    }


    fun takePicture() {

        Toast.makeText(this, "開始拍照", Toast.LENGTH_SHORT).show()

    }


}

可以看到,這裡先是透過呼叫 requestPermissions() 方法請求相機許可權和定位許可權,然後在 onRequestPermissionsResult() 方法裡監聽授權的結果。如果使用者同意了這兩個許可權,那麼我們就可以去進行拍照了,如果使用者拒絕了任意一個許可權,那麼彈出一個 Toast 提示,告訴使用者某項許可權被拒絕了,從而無法進行拍照。

這種寫法麻煩嗎?這個就仁者見仁智者見智了,有些朋友可能覺得這也沒多少行程式碼呀,有什麼麻煩的。但我個人認為還是比較麻煩的,每次需要請求執行時許可權時,我都會覺得很心累,不想寫這麼囉嗦的程式碼。

不過我們暫時不從簡易性的角度考慮,從正確性的角度上來講,這種寫法對嗎?我認為是有問題的,因為我們在許可權被拒絕時只是彈了一個 Toast 來提醒使用者,並沒有提供後續的操作方案,使用者如果真的拒絕了某個許可權,應用程式就無法繼續使用了。

因此,我們還需要提供一種機制,當許可權被使用者拒絕時,可以再次重新請求許可權。

現在我對程式碼進行如下修改:

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        requestPermissions()

    }


    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {

        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {

            1 -> {

                var allGranted = true

                for (result in grantResults) {

                    if (result != PackageManager.PERMISSION_GRANTED) {

                        allGranted = false

                    }

                }

                if (allGranted) {

                    takePicture()

                } else {

                    AlertDialog.Builder(this).apply {

                        setMessage("拍照功能需要您同意相機和定位許可權")

                        setCancelable(false)

                        setPositiveButton("確定") { _, _ ->

                            requestPermissions()

                        }

                    }.show()

                }

            }

        }

    }


    fun requestPermissions() {

        ActivityCompat.requestPermissions(this,

            arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1)

    }


    fun takePicture() {

        Toast.makeText(this, "開始拍照", Toast.LENGTH_SHORT).show()

    }


}

這裡我將請求許可權的程式碼提取到了一個 requestPermissions() 方法當中,然後在 onRequestPermissionsResult() 裡判斷,如果使用者拒絕了某項許可權,那麼就彈出一個對話方塊,告訴使用者相機和定位許可權是必須的,然後在 setPositiveButton 的點選事件中呼叫 requestPermissions() 方法重新請求許可權。

我們來看一下現在的執行效果:

圖片描述

可以看到,現在我們對許可權被拒絕的場景進行了更加充分的考慮。

那麼現在這種寫法,是不是就將請求執行時許可權的各種場景都考慮周全了呢?其實還沒有,因為 Android 許可權系統還提供了一種非常 “噁心” 的機制,叫拒絕並不再詢問。

當某個許可權被使用者拒絕了一次,下次我們如果再申請這個許可權的話,介面上會多出一個拒絕並不再詢問的選項。只要使用者選擇了這一項,那麼完了,我們之後都不能再去請求這個許可權了,因為系統會直接返回我們許可權被拒絕。

這種機制對於使用者來說非常友好,因為它可以防止一些惡意軟體流氓式地無限重複申請許可權,從而嚴重騷擾使用者。但是對於開發者來說,卻讓我們苦不堪言,如果我的某項功能就是必須依賴於這個許可權才能執行,現在使用者把它拒絕並不再詢問了,我該怎麼辦?

當然,絕大多數的使用者都不是傻 X,當然知道拍照功能需要用到相機許可權了,相信 99% 的使用者都會點選同意授權。但是我們可以不考慮那剩下 1% 的使用者嗎?不可以,因為你們公司的測試就是那 1% 的使用者,他們會進行這種傻 X 式的操作。

也就是說,即使只為了那 1% 的使用者,為了這種不太可能會出現的操作方式,我們在程式中還是得要將這種場景充分考慮進去。

那麼,許可權被拒絕且不再詢問了,我們該如何處理呢?比較通用的處理方式就是提醒使用者手動去設定當中開啟許可權,如果想做得再好一點,可以提供一個自動跳轉到當前應用程式設定介面的功能。

下面我們就來針對這種場景進行完善,如下所示:

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        requestPermissions()

    }


    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {

        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {

            1 -> {

                val denied = ArrayList()

                val deniedAndNeverAskAgain = ArrayList()

                grantResults.forEachIndexed { index, result ->

                    if (result != PackageManager.PERMISSION_GRANTED) {

                        if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[index])) {

                            denied.add(permissions[index])

                        } else {

                            deniedAndNeverAskAgain.add(permissions[index])

                        }

                    }

                }

                if (denied.isEmpty() && deniedAndNeverAskAgain.isEmpty()) {

                    takePicture()

                } else {

                    if (denied.isNotEmpty()) {

                        AlertDialog.Builder(this).apply {

                            setMessage("拍照功能需要您同意相簿和定位許可權")

                            setCancelable(false)

                            setPositiveButton("確定") { _, _ ->

                                requestPermissions()

                            }

                        }.show()

                    } else {

                        AlertDialog.Builder(this).apply {

                            setMessage("您需要去設定當中同意相簿和定位許可權")

                            setCancelable(false)

                            setPositiveButton("確定") { _, _ ->

                                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)

                                val uri = Uri.fromParts("package", packageName, null)

                                intent.data = uri

                                startActivityForResult(intent, 1)

                            }

                        }.show()

                    }

                }

            }

        }

    }


    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {

            1 -> {

                requestPermissions()

            }

        }

    }


    fun requestPermissions() {

        ActivityCompat.requestPermissions(this,

            arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1)

    }


    fun takePicture() {

        Toast.makeText(this, "開始拍照", Toast.LENGTH_SHORT).show()

    }


}

現在程式碼已經變得比較長了,我還是帶著大家來梳理一下。

這裡我在 onRequestPermissionsResult() 方法中增加了 denied 和 deniedAndNeverAskAgain 兩個集合,分別用於記錄拒絕和拒絕並不再詢問的許可權。如果這兩個集合都為空,那麼說明所有許可權都被授權了,這時就可以直接進行拍照了。

而如果 denied 集合不為空,則說明有許可權被使用者拒絕了,這時候我們還是彈出一個對話方塊來提醒使用者,並重新申請許可權。而如果 deniedAndNeverAskAgain 不為空,說明有許可權被使用者拒絕且不再詢問,這時就只能提示使用者去設定當中手動開啟許可權,我們編寫了一個 Intent 來執行跳轉邏輯,並在 onActivityResult() 方法,也就是使用者從設定回來的時候重新申請許可權。

那麼現在執行一下程式,效果如下圖所示:

圖片描述

可以看到,當我們第一次拒絕許可權的時候,會提醒使用者,相機和定位許可權是必須的。而如果使用者繼續置之不理,選擇拒絕並不再詢問,那麼我們將提醒使用者,他必須手動開戶這些許可權才能繼續執行程式。

到現在為止,我們才算是把一個 “簡單” 的許可權請求流程用比較完善的方式處理完畢。然而程式碼寫到這裡真的還算是簡單嗎?每次申請執行時許可權,都要寫這麼長長的一段程式碼,你真的受得了嗎?

這也就是我編寫 PermissionX 這個開源庫的原因,在 Android 中請求許可權從來都不是一件簡單的事情,但它不應該如此複雜

PermissionX 將請求執行時許可權時那些應該考慮的複雜邏輯都封裝到了內部,只暴露最簡單的介面給開發者,從而讓大家不需要考慮上面我所討論的那麼多場景。

而我們使用 PermissionX 來實現和上述一模一樣的功能,只需要這樣寫就可以了:

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        PermissionX.init(this)

            .permissions(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)

            .onExplainRequestReason { scope, deniedList ->

val message = "拍照功能需要您同意相簿和定位許可權"

val ok = "確定"

                scope.showRequestReasonDialog(deniedList, message, ok)

            }

            .onForwardToSettings { scope, deniedList ->

val message = "您需要去設定當中同意相簿和定位許可權"

val ok = "確定"

                scope.showForwardToSettingsDialog(deniedList, message, ok)

            }

            .request { _, _, _ ->

                takePicture()

            }

    }


    fun takePicture() {

        Toast.makeText(this, "開始拍照", Toast.LENGTH_SHORT).show()

    }


}

可以看到,請求許可權的程式碼一下子變得極其精簡。

我們只需要在 permissions() 方法中傳入要請求的許可權名,在 onExplainRequestReason() 和 onForwardToSettings() 回撥中填寫對話方塊上的提示資訊,然後在 request() 回撥中即可保證已經得到了所有請求許可權的授權,呼叫 takePicture() 方法開始拍照即可。

透過這樣的直觀對比大家應該能感受到 PermissionX 所帶來的便利了吧?上面那段長長的請求許可權的程式碼我真的是為了給大家演示才寫的,而我再也不想寫第二遍了。

另外,本篇文章主要只是演示了一下 PermissionX 的易用性,並不涉及其中具體的諸多用法,如 Android 11 相容性,自定義對話方塊樣式等等。如果大家感興趣的話,更多用法請參考下面的連結。

在專案中引入 PermissionX 也非常簡單,只需要新增如下的依賴即可:

dependencies {

    ...

    implementation 'com.permissionx.guolindev:permissionx:1.3.1'

}


作者:郭霖
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4692/viewspace-2796825/,如需轉載,請註明出處,否則將追究法律責任。

相關文章