Android 6.0 Marshmallow : The New Runtime Permission ( Part 2 )
Bài đăng này đã không được cập nhật trong 3 năm
Như mình đã giới thiệu ở Phần 1 https://viblo.asia/bui.huu.tuan/posts/AeJ1vO2PGkby , trong Phần 2 này mình sẽ hướng dẫn các bạn xử lí Runtime Permission một cách cụ thế.
1. Các Permission được tự động cấp phép
Dưới đây là danh sách các Permission được tự động cấp phép lúc cài đặt và sẽ không bị thu hồi. Chúng được gọi là Normal Permission (PROTECTION_NORMAL) :
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT
Chỉ cần đặt chúng trong file AndroidManifest.xml như bạn vẫn làm trước đây vì những Permission trên sẽ không bị thu hồi như đã đề cập.
2. Sẵn sàng với Runtime Permission
Bây giờ là lúc làm cho Ứng dụng của bạn hỗ trợ Runtime Permisson một cách hoàn hảo. Đầu tiên hãy để compileSdkVersion và targetSdkVersion là 23.
android {
compileSdkVersion 23
...
defaultConfig {
...
targetSdkVersion 23
...
}
Ví dụ dưới đây, chúng ta muốn thêm một danh bạ :
private static final String TAG = "Contacts";
private void insertDummyContact() {
// Two operations are needed to insert a new contact.
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);
// First, set up a new raw contact.
ContentProviderOperation.Builder op =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
operations.add(op.build());
// Next, set the name for the contact.
op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"__DUMMY CONTACT from runtime permissions sample");
operations.add(op.build());
// Apply the operations.
ContentResolver resolver = getContentResolver();
try {
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (RemoteException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
} catch (OperationApplicationException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
}
}
Đoạn code trên yêu cầu Permission WRITE_CONTACTS. Nếu nó được gọi ra mà không được quyền cấp phép, ứng dụng sẽ bị crash.
Tất nhiên, bạn sẽ thêm Permission trên vào file AndroidManifest.xml như bạn vẫn làm trước đây :
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
Bước tiếp theo chúng ta sẽ viết 1 hàm để kiểm tra xem Permission đó có được cấp phép hay không. Nếu nó không được cấp phép, ta sẽ hiển thị một dialog yêu cầu người dùng cấp phép cho nó. Còn nếu nó đã được cấp phép rồi thì thực hiện follow tiếp theo như bình thường.
Permission được xếp theo các nhóm sau :
Nếu bất cứ Permission nào nằm trong một nhóm được cấp phép, thì các Permission còn lại trong nhóm đó cũng sẽ được tự động cấp phép. Trong trường hợp này, WRITE_CONTACTS được cấp phép, có nghĩa là 2 Permission còn lại trong nhóm là READ_CONTACTS và GET_ACCOUNTS cũng sẽ được tự động cấp phép.
Code để kiểm tra và yêu cầu cấp phép Permission như sau :
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
Nếu Permission đã được cấp phép, hàm insertDummyContact() sẽ được gọi. Ngược lại, hàm requestPermissions sẽ được gọi và hiển thị một dialog như sau :
Bất kể người dùng chọn Deny hay Allow, hàm onRequestPermissionsResult sẽ luôn được gọi để thông báo kết qủa mà chúng ta có thể kiểm tra thông qua tham số thứ 3, grantResults, như sau :
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission Granted
insertDummyContact();
} else {
// Permission Denied
Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
Đó là cách Runtime Permission hoạt động. Code khá đơn giarn, nhưng để sử dụng nó làm cho ứng dụng chạy một cách hoàn hảo, bạn phải xử lí tất cả trường hợp với cùng một phương thức như trên.
3. Xử lí "Never Ask Again"
Nếu người dùng từ chối cấp phép một Permission, khi khởi chạy lần thứ hai, người dùng sẽ có một lựa chọn "Never ask again" để tránh việc ứng dụng yêu cầu người dùng trong lần tiếp theo.
Nếu người dùng chọn "Never Ask Again" trước khi ấn Deny, lần tiếp theo khi chúng ta gọi hàm requestPermissions, dialog sẽ không xuất hiện nữa, thay vì đó, nó sẽ ko làm gì hết.
Tuy nhiên nó cũng khá bất tiện nếu người dùng ko làm gì và ko có gì tương tác ngược trở lại. Trong trường hợp này bạn phải xử lí như sau. Trước khi gọi requestPermissions, chúng ta phải kiểm tra xem chúng ta có nên nói lý do tại sao ứng dụng cần được cấp phép Permission hay không, thông qua hàm shouldShowRequestPermissionRationale :
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
Kết qủa là dialog sẽ được hiển thị ra khi Permisssion yêu cầu lần đầu tiên và cũng được hiển thị ra nếu người dùng trước đó đã chọn Never ask again. Đối với các trường hợp sau đó, onRequestPermissionsResult sẽ được gọi với PERMISSION_DENIED, mà ko hiển thị bất cứ dialog yêu cầu Permission nào.
Yeah. Xong
4. Yêu cầu nhiều Permission cùng một lúc
Chắc chắn rằng sẽ có một số tính năng của ứng dụng yêu cầu nhiều hơn một Permission. Bạn có thể yêu cầu nhiều Permission cùng một lúc với hàm đã viết ở trên. Và hãy luôn nhớ kiểm tra trường hợp người dùng chọn "Never ask again" cho từng Permission.
final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
private void insertDummyContactWrapper() {
List<String> permissionsNeeded = new ArrayList<String>();
final List<String> permissionsList = new ArrayList<String>();
if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
permissionsNeeded.add("GPS");
if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
permissionsNeeded.add("Read Contacts");
if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
permissionsNeeded.add("Write Contacts");
if (permissionsList.size() > 0) {
if (permissionsNeeded.size() > 0) {
// Need Rationale
String message = "You need to grant access to " + permissionsNeeded.get(0);
for (int i = 1; i < permissionsNeeded.size(); i++)
message = message + ", " + permissionsNeeded.get(i);
showMessageOKCancel(message,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return;
}
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
return;
}
insertDummyContact();
}
private boolean addPermission(List<String> permissionsList, String permission) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
permissionsList.add(permission);
// Check for Rationale Option
if (!shouldShowRequestPermissionRationale(permission))
return false;
}
return true;
}
Khi mỗi Permission nhận đc kết qủa cấp phép, kết quả đó đều được nhận về thông qua hàm onRequestPermissionsResult. Ta sử dụng HashMap để nhìn code dễ đọc và rõ ràng hơn :
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
{
Map<String, Integer> perms = new HashMap<String, Integer>();
// Initial
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
// Fill with results
for (int i = 0; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// Check for ACCESS_FINE_LOCATION
if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
// All Permissions Granted
insertDummyContact();
} else {
// Permission Denied
Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
.show();
}
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
Điều kiện là linh hoạt. Trong một số trường hợp, nếu Permission không được cấp phép, tính năng tương ứng sẽ không hoạt động. Nhưng cũng trong một số trường hợp, tính năng sẽ hoạt động ở mức giowis hạn. App sẽ vận hành như thế nào là tùy thuộc vào bạn.
5. Sử dụng Support Library
Mặc dù code nói trên hoạt động tốt trên Android 6.0 Marshmallow, nhưng nó sẽ bị crash trên các thiết bị chạy các phiên bản trước Android 6.0, bởi vì các hàm trên được thêm vào từ API Level 23.
Cách trực tiếp là bạn hãy kiểm tra Build Version :
if (Build.VERSION.SDK_INT >= 23) {
// Marshmallow+
} else {
// Pre-Marshmallow
}
Support Library v4 cũng có sẵn một số hàm phục vụ việc này :
`ContextCompat.checkSelfPermission()
Bất kể ứng dụng chạy trên Android M hoặc không. Hàm này sẽ trả về PERMISSION_GRANTED nếu ứng dụng được cấp phép. Ngược lại , PERMISSION_DENIED sẽ được trả về.
`ActivityCompat.requestPermissions()
Hàm này dùng trên các phiên bản trước Android M, OnRequestPermissionsResultCallback sẽ trả về kết qủa PERMISSION_GRANTED hoặc PERMISSION_DENIED.
`ActivityCompat.shouldShowRequestPermissionRationale()
Hàm này dùng trên các phiên bản trước Android M, nó sẽ luôn luôn trả về false.
6. Điều gì xảy ra nếu Permission bị thu hồi trong khi ứng dụng đang chạy
Như mình đã đề cập, Permission có thể bị thu hồi bất cứ lúc nào thông qua mục Settings của thiết bị.
Vậy điều gì sẽ xảy ra nếu Permission bị thu hồi trong khi ứng dụng đang chạy ? Mình đã thử và rất tiếc là ứng dụng bị dừng lại. Dường như rằng hệ điều hành đã ịlàm điều đó khi mà Permission bị thu hồi.
7.Kết luận và Đề xuất
Mình tin rằng các bạn đã có một cái nhìn rõ ràng về hệ thống Permission mới và các bạn cũng đã nhận ra vấn đề bất cập như thế nào với chúng.
Tuy nhiên chúng ta không có sự chọn. Runtime Permission đã được sử dụng trong Android Marshmallow. Điều duy nhất chúng ta có thể làm là để cho ứng dụng hỗ trợ đầy đủ hệ thống Permission mới này.
Rất may là chỉ có một số Permission nằm trong Runtime Permission. Hầu hết các Permission thông dụng, ví dụ INTERNET, là Normal Permission , chúng được tự động cấp phép, bạn không cần phải làm gì với chúng.
Có 2 đề xuất mà mình muốn nhắc tới.
- Hãy để Runtime Permission hỗ trợ một vấn đề cấp bách.
- Đừng để targetSdkVersion là 23 khi ứng dụng của bạn chưa hỗ trợ đầy đủ Runtime Permission. Đặc biệt khi bạn tạo mới Project, đừng quên để nhìn xem build.gradle đang để targetSdkVersion là bao nhiêu.
Happy Coding !
Tham khảo http://inthecheesefactory.com/blog/things-you-need-to-know-about-android-m-permission-developer-edition/en https://developer.android.com/intl/vi/training/permissions/index.html
All rights reserved