最後更新日期:2023 年 03 月 16 日

權限概述

權限 (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()
        }
    }
}

參考資料

Android developers Guide: Permissions on Android

Android developers Guide: Getting a result from an activity

Android developers Reference: android.Manifest.permission

Philipp: PERMISSIONS – Android Fundamentals

Philipp: Handling Location Permissions – MVVM Running Tracker App – Part 9

第 12 屆 iThome 鐵人賽:{Day26}Activity

**2021 iThome 鐵人賽:110/01 – 什麼!startActivityForResult 被標記棄用?

Last modified: 2023 年 3 月 16 日

Author

Comments

Write a Reply or Comment

Your email address will not be published.