內容目錄
權限概述
權限 (Permission)機制有兩個主要的用途:
1、保護重要資料
2、避免危險的行為
權限的分類
危險權限
在 app 裏,有些功能需要受到限制,例如:直接撥打電話 (Intent.ACTION_CALL,對應到 Manifest.permission.CALL_PHONE),因為這會產生費用,我們把它到歸類到「危險權限」。
屬於危險權限的功能,我們要先在 AndroidManifest.xml 宣告,還需要在運行期間,請求使用者同意該危險權限。
普通權限
有些功能則不是那麼的危險,例如:網路的連結 (Manifest.permission.INTERNET),這一類稱為「普通權限」。
屬於一般權限的功能,我們若要使用,需要在 AndroidManifest.xml 中宣告。
另外,還有些功能則不需加以限制,例如:顯示撥號介面(Intent.ACTION_DIAL),就不需要用到權限的機制。
所有權限列表:android.Manifest.permission
程式範例
程式範例會使用到 view binding,請參考 Android – View Binding 的使用方式
未設好權限的狀況
先建立一個有 EmptyActivity 的專案,命名為 Permission Lab01,我們先不去設定權限,觀察一下會是啥狀況。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:layout_marginTop="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_dial" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Dial" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" /> </androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnDial.setOnClickListener { call() } } private fun call() { try { val intent = Intent(Intent.ACTION_CALL) intent.data = Uri.parse("tel:0800092000") startActivity(intent) } catch(e:SecurityException) { e.printStackTrace() } } }
沒有在 AndroidMainfest.xml 設定好,或是沒有處理執行時期權限請求,都會有 java.lang.SecurityException: Permission Denial
的錯誤訊息,範例如下:
2022-10-13 07:36:30.205 22178-22178/app.kirin.android.permissionlab01 E/AndroidRuntime: FATAL EXCEPTION: main Process: app.kirin.android.permissionlab01, PID: 22178 java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxx cmp=com.android.server.telecom/.components.UserCallActivity } from ProcessRecord{f9e98f3 22178:app.kirin.android.permissionlab01/u0a20} (pid=22178, uid=10020) requires android.permission.CALL_PHONE at android.os.Parcel.createExceptionOrNull(Parcel.java:2437) at android.os.Parcel.createException(Parcel.java:2421) at android.os.Parcel.readException(Parcel.java:2404) at android.os.Parcel.readException(Parcel.java:2346) at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:2897) at android.app.Instrumentation.execStartActivity(Instrumentation.java:1743) at android.app.Activity.startActivityForResult(Activity.java:5473) at androidx.activity.ComponentActivity.startActivityForResult(ComponentActivity.java:712) at android.app.Activity.startActivityForResult(Activity.java:5431) at androidx.activity.ComponentActivity.startActivityForResult(ComponentActivity.java:693) at android.app.Activity.startActivity(Activity.java:5817) at android.app.Activity.startActivity(Activity.java:5770) at app.kirin.android.permissionlab01.MainActivity.onCreate$lambda-0(MainActivity.kt:23) at app.kirin.android.permissionlab01.MainActivity.$r8$lambda$RmyjueAAZ38wmv3rF1YjUvXZHeo(Unknown Source:0) at app.kirin.android.permissionlab01.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2) at android.view.View.performClick(View.java:7792) at android.widget.TextView.performClick(TextView.java:16112) at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1194) at android.view.View.performClickInternal(View.java:7769) at android.view.View.access$3800(View.java:910) at android.view.View$PerformClick.run(View.java:30218) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:226) at android.os.Looper.loop(Looper.java:313) at android.app.ActivityThread.main(ActivityThread.java:8751) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135) Caused by: android.os.RemoteException: Remote stack trace: at com.android.server.wm.ActivityTaskSupervisor.checkStartAnyActivityPermission(ActivityTaskSupervisor.java:1334) at com.android.server.wm.ActivityStarter.executeRequest(ActivityStarter.java:1275) at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:906) at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1868) at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1739)
處理權限宣告
在 AndroidMainfest.xml 中加入
<uses-permission android:name="android.permission.CALL_PHONE" />
完整 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="<http://schemas.android.com/apk/res/android>" package="app.kirin.android.permissionlab01"> <uses-permission android:name="android.permission.CALL_PHONE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.PermissionLab01"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
處理執行時期權限請求 – 方法一
使用 ActivityCompat.requestPermissions() 配合 onRequestPermissionsResult()
這是比較早期會用的方式,現在官方推薦使用方法二。
MainActivity.kt
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnDial.setOnClickListener { if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1) } else { call() } } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when(requestCode) { 1-> { if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { call() } else { Toast.makeText(this, "You must grant permission", Toast.LENGTH_SHORT).show() } } } } private fun call() { try { val intent = Intent(Intent.ACTION_CALL) intent.data = Uri.parse("tel:0225572468") startActivity(intent) } catch(e:SecurityException) { e.printStackTrace() } } }
處理執行時期權限請求 – 方法二 – 使用 Activity Result API
使用 registerForActivityResult()
注意事項
registerForActivityResult() 的呼叫必須在 Activiy 或 Fragment 創建完成前(也就是不能到達CREATED) ,例如,我們不能把呼叫放在 Button 的 setOnClickListener 中,如果這樣做,app 會丟出錯誤,告訴我們不能在 RESUME 階段中呼叫 registerForActivityResult() 。
單一權限請求
使用 ActivityResultContracts.RequestPermission(),其回傳值為 Boolean
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { result-> if(result) { call() } else { Toast.makeText(this, "You must grant permission", Toast.LENGTH_SHORT).show() } } binding.btnDial.setOnClickListener { if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { permissionLauncher.launch(Manifest.permission.CALL_PHONE) } else { call() } } } private fun call() { try { val intent = Intent(Intent.ACTION_CALL) intent.data = Uri.parse("tel:0225572468") startActivity(intent) } catch(e:SecurityException) { e.printStackTrace() } } }
多權限請求
使用 ActivityResultContracts.RequestMultiplePermissions(),其回傳值為 Array
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var permissionsLauncher: ActivityResultLauncher<Array<String>> private var callPermissionGranted = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) permissionsLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions()) { permisisons -> // 如果必須請求多個權限,可在此處理回傳的結果 callPermissionGranted = permisisons[Manifest.permission.CALL_PHONE] ?: callPermissionGranted } binding.btnDial.setOnClickListener { if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { try { requestPermission() } catch(e:Exception) { e.printStackTrace() } } else { call() } } } fun requestPermission() { val permissionsToRequest = mutableListOf<String>() // 如果必須請求多個權限,可在此加入 permissionsToRequest.add(Manifest.permission.CALL_PHONE) if(permissionsToRequest.isNotEmpty()) { permissionsLauncher.launch(permissionsToRequest.toTypedArray()) } } private fun call() { try { val intent = Intent(Intent.ACTION_CALL) intent.data = Uri.parse("tel:0225572468") startActivity(intent) } catch(e:SecurityException) { e.printStackTrace() } } }
Comments