權限概述
權限 (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