+2

Android 6.0 Marshmallow : The New Runtime Permission ( Part 2 )

requestpermission.jpg

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 :

permgroup.png

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 :

requestpermission.jpg

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.

neveraskagain.jpg

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.

rationaledialog.jpg

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ị.

permissionsrevoke.jpg

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.

  1. Hãy để Runtime Permission hỗ trợ một vấn đề cấp bách.
  2. Đừ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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí