Chào mừng bạn đến với ví dụ về runtime permissions trong Android (quyền thời gian chạy). Kể từ phiên bản Android 6.0 Marshmallow, Google đã thay đổi cách quản lý permissions trong ứng dụng. Trong hướng dẫn này, chúng ta sẽ tìm hiểu về cơ chế cấp quyền mới được giới thiệu và cách xử lý chúng. Nếu không xử lý đúng, ứng dụng có thể bị sập.

Android runtime permissions là gì?
Từ Android 6.0 (SDK 23), người dùng sẽ được yêu cầu cấp một số quyền cụ thể ngay trong quá trình sử dụng ứng dụng, khi những quyền đó thực sự cần thiết. Vậy câu hỏi đặt ra là: Các ứng dụng cũ có chạy được trên Android Marshmallow không? Câu trả lời là có, nếu targetSdkVersion là 22 hoặc thấp hơn, tức là Android runtime permissions có hỗ trợ tương thích ngược. Tuy nhiên, điều này không có nghĩa là ta có thể dùng mô hình quyền cũ bằng cách đặt SDK version là 22. Một người dùng Marshmallow vẫn có thể thu hồi các quyền nguy hiểm (dangerous permissions) thông qua Cài đặt → Ứng dụng → Quyền truy cập (Settings → Apps → Permissions). Nếu chúng ta gọi một hàm yêu cầu quyền mà người dùng chưa cấp, chương trình sẽ lập tức ném ra một Exception (java.lang.SecurityException) và khiến ứng dụng bị sập. Vì vậy, chúng ta cần triển khai mô hình cấp quyền mới này trong ứng dụng của mình.
Quyền nguy hiểm (dangerous permissions) và quyền thông thường (normal permissions) trong Android
Android phân loại các quyền thành hai nhóm: quyền nguy hiểm và quyền thông thường. Cả hai loại đều phải được khai báo trong tệp Manifest. Tuy nhiên, từ Android 6.0 trở đi, chỉ các quyền nguy hiểm mới cần được kiểm tra lúc chạy, còn quyền thông thường thì không. Ví dụ quyền thông thường: android.permission.INTERNET.
Các quyền nguy hiểm được nhóm theo danh mục để người dùng dễ hiểu hơn về những gì họ đang cho phép. Nếu người dùng đồng ý với một quyền trong nhóm, thì đồng nghĩa với việc đồng ý với toàn bộ nhóm đó. Một ví dụ về quyền nguy hiểm là android.permission.FINE_LOCATION và android.permission.COARSE_LOCATION. Bật bất kỳ quyền nào cho vị trí cũng sẽ bật tất cả.
Yêu cầu runtime permissions trong Android
Phương thức requestPermissions(String[] permissions, int requestCode); là một phương thức công khai dùng để yêu cầu các quyền nguy hiểm. Bạn có thể yêu cầu nhiều quyền nguy hiểm cùng lúc bằng cách truyền vào một mảng String chứa danh sách các quyền.
Lưu ý: Nếu các quyền thuộc hai nhóm khác nhau, mỗi quyền sẽ hiển thị một hộp thoại yêu cầu riêng. Nếu các quyền nằm cùng một nhóm, chỉ hiển thị một hộp thoại. Kết quả của yêu cầu quyền sẽ được trả về trong phương thức: onRequestPermissionResult. Ví dụ: Nếu bạn muốn muốn truy cập camera và vị trí trong ứng dụng, cả hai đều là quyền nguy hiểm. Khi ứng dụng được khởi chạy, bạn sẽ hiển thị một hộp thoại yêu cầu người dùng cấp quyền. Ta sẽ thêm các quyền đó vào một mảng chuỗi, sau đó gọi requestPermissions() như đoạn code bên dưới.
String[] perms = {"android.permission.FINE_LOCATION", "android.permission.CAMERA"};
int permsRequestCode = 200;
requestPermissions(perms, permsRequestCode);
@Override
public void onRequestPermissionsResult(int permsRequestCode, String[] permissions, int[] grantResults){
switch(permsRequestCode){
case 200:
boolean locationAccepted = grantResults[0]==PackageManager.PERMISSION_GRANTED;
boolean cameraAccepted = grantResults[1]==PackageManager.PERMISSION_GRANTED;
break;
}
}
Bây giờ, chúng ta không muốn người dùng phải liên tục chấp nhận lại những quyền mà họ đã từng cấp. Ngay cả khi quyền đã được cấp trước đó, vẫn cần kiểm tra lại để đảm bảo rằng người dùng chưa thu hồi quyền đó sau này. Để làm điều này, cần gọi phương thức sau cho từng quyền.
checkSelfPermission(String perm);
Phương thức này trả về một giá trị PERMISSION_GRANTED hoặc PERMISSION_DENIED. Lưu ý: Nếu người dùng từ chối một quyền quan trọng đối với ứng dụng, thì có thể dùng shouldShowRequestPermissionRationale(String permission);để giải thích cho người dùng lý do cần quyền đó. Chúng ta sẽ xây dựng một ứng dụng có chức năng kiểm tra quyền. Nếu chưa được cấp, ứng dụng sẽ yêu cầu cấp quyền tại thời điểm runtime.
Cấu trúc dự án Android Runtime Permissions

Mã nguồn Android Runtime Permissions
content_main.xml chứa hai nút kiểm tra quyền và yêu cầu quyền.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:app="<https://schemas.android.com/apk/res-auto>"
xmlns:tools="<https://schemas.android.com/tools>"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.journaldev.runtimepermissions.MainActivity"
tools:showIn="@layout/activity_main">
<Button
android:id="@+id/check_permission"
android:layout_width="match_parent"
android:layout_centerInParent="true"
android:layout_height="wrap_content"
android:text="Check Permission"/>
<Button
android:id="@+id/request_permission"
android:layout_below="@+id/check_permission"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Request Permission"/>
</RelativeLayout>
Tệp MainActivity.java được định nghĩa như sau.
package com.journaldev.runtimepermissions;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.CAMERA;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final int PERMISSION_REQUEST_CODE = 200;
private View view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Button check_permission = (Button) findViewById(R.id.check_permission);
Button request_permission = (Button) findViewById(R.id.request_permission);
check_permission.setOnClickListener(this);
request_permission.setOnClickListener(this);
}
@Override
public void onClick(View v) {
view = v;
int id = v.getId();
switch (id) {
case R.id.check_permission:
if (checkPermission()) {
Snackbar.make(view, "Permission already granted.", Snackbar.LENGTH_LONG).show();
} else {
Snackbar.make(view, "Please request permission.", Snackbar.LENGTH_LONG).show();
}
break;
case R.id.request_permission:
if (!checkPermission()) {
requestPermission();
} else {
Snackbar.make(view, "Permission already granted.", Snackbar.LENGTH_LONG).show();
}
break;
}
}
private boolean checkPermission() {
int result = ContextCompat.checkSelfPermission(getApplicationContext(), ACCESS_FINE_LOCATION);
int result1 = ContextCompat.checkSelfPermission(getApplicationContext(), CAMERA);
return result == PackageManager.PERMISSION_GRANTED && result1 == PackageManager.PERMISSION_GRANTED;
}
private void requestPermission() {
ActivityCompat.requestPermissions(this, new String[]{ACCESS_FINE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSION_REQUEST_CODE:
if (grantResults.length > 0) {
boolean locationAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
boolean cameraAccepted = grantResults[1] == PackageManager.PERMISSION_GRANTED;
if (locationAccepted && cameraAccepted)
Snackbar.make(view, "Permission Granted, Now you can access location data and camera.", Snackbar.LENGTH_LONG).show();
else {
Snackbar.make(view, "Permission Denied, You cannot access location data and camera.", Snackbar.LENGTH_LONG).show();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (shouldShowRequestPermissionRationale(ACCESS_FINE_LOCATION)) {
showMessageOKCancel("You need to allow access to both the permissions",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{ACCESS_FINE_LOCATION, CAMERA},
PERMISSION_REQUEST_CODE);
}
}
});
return;
}
}
}
}
break;
}
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
}
Lưu ý: Thêm các quyền cần được kiểm tra lúc chạy vào tệp Manifest, đặt phía trên thẻ application như sau:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Trong đoạn code trên, hai quyền được kiểm tra và yêu cầu là CAMERA và LOCATION. Việc import tên lớp đầy đủ của quyền dưới dạng static cho phép chúng ta chỉ cần viết tên hằng số PERMISSION mà không cần ghi đầy đủ đường dẫn. Phương thức checkPermission() sẽ gọi checkSelfPermission cho từng quyền cụ thể. Phương thức requestPermission() gọi ActivityCompat.requestPermissions(this, new String[]{ACCESS_FINE_LOCATION, CAMERA}, PERMISSION_REQUEST_CODE);. Phương thức onRequestPermissionsResult sẽ kiểm tra xem các quyền có được cấp hay không. Trong đoạn mã của chúng ta, nếu cả hai quyền không được cấp, một hộp thoại cảnh báo (alert dialog) sẽ xuất hiện để thông báo rằng việc yêu cầu cấp quyền là bắt buộc. Để làm điều đó, phương thức shouldShowRequestPermissionRationale(String permission) sẽ được gọi, và nó sẽ kích hoạt một hộp thoại giải thích lý do cần quyền đó. Bạn cũng có thể thu hồi các quyền này thủ công thông qua: Cài đặt → Ứng dụng → Quyền (Settings → Apps → Permissions). Lưu ý: Các phương thức liên quan đến quyền truy cập lúc chạy chỉ khả dụng kể từ API 23. Vì vậy, điều kiện sau cần được kiểm tra trong mỗi phương thức:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
Hình ảnh và kết quả đầu ra của ứng dụng ví dụ sẽ được hiển thị phía dưới.

Hướng dẫn đến đây là kết thúc. Bạn có thể tải xuống dự án Android Runtime Permissions hoàn chỉnh từ liên kết bên dưới.