สิ่งที่นักพัฒนาต้องรู้เกี่ยวกับระบบ Runtime Permission ใหม่ของแอนดรอยด์ รีบปรับโค้ดก่อนจะสายเกินไป

Posted on 26 Aug 2015 05:16 | 51342 reads | 0 shares
 

Android M ประกาศชื่ออย่างเป็นทางการแล้ว และคงอีกไม่นานที่มันจะถูกปล่อยให้คนได้อัพเดตและใช้งานจริงกัน

ทั้งนี้ ถึงแม้แอนดรอยด์จะมีการพัฒนาออกรุ่นใหม่มาอย่างต่อเนื่อง แต่สำหรับการเปลี่ยนมาสู่ Android M นี้ต่างกันเพราะมันมี Major Change ระดับพลิกโลกอย่างระบบ Runtime Permission มาด้วย น่าแปลกใจที่ระบบนี้ไม่ได้รับการพูดถึงแบบเน้นหนักมากนักในแง่ของนักพัฒนาทั้งๆที่มันอาจสร้างปัญหาใหญ่โตได้ในเวลาอันใกล้นี้

วันนี้เราเลยจะเอาเรื่องของ Permission แบบใหม่บนแอนดรอยด์นี้มาเล่าให้ฟังโดยละเอียดและจะพาไปปรับโค้ดให้สนับสนุนระบบใหม่นี้ด้วยกันก่อนจะสายเกินไปครับ

รู้จักกับระบบ Permission แบบใหม่

ปัญหาทางด้านความปลอดภัยที่เจอมาตลอดของแอนดรอยด์ ส่วนหนึ่งเกิดจากการระบบ Permission ที่มีการขอตั้งแต่ตอนติดตั้งแอพฯ และหลังจากนั้นแอพฯก็จะสามารถเข้าถึงทุกอย่างตามที่ขอได้ทันที โดยที่ผู้ใช้ไม่มีทางรู้เลยว่านักพัฒนาใช้สิทธิ์เหล่านั้นในการทำอะไรบ้าง

ไม่ต้องแปลกใจที่มีผู้ไม่หวังดีจำนวนมาก ใช้ช่องทางตรงนี้ในการแอบเก็บข้อมูลส่วนตัวของผู้ใช้และส่งเข้าไป Server เพื่อหาผลประโยชน์

มันน่ากลัวแค่ไหน ผมเคยทำวีดีโออธิบายตัวอย่างไว้อันนึง อยากให้ลองดูกันครับ แล้วจะเห็นภาพว่ามันน่ากลัวยังไง

ทางทีมแอนดรอยด์ก็เห็นปัญหาตรงนี้เช่นกัน ล่าสุดหลังจากที่แอนดรอยด์ปล่อยมา 7 ปีและพัฒนามาจนถึงเวอร์ชั่น 6.0 บน Android Marshmallow ก็ปรับเปลี่ยนวิธีในการจัดการกับ Permission ใหม่หมดเป็นที่เรียบร้อย จากเดิมที่แอพฯจะได้รับ Permission ก่อนแล้วจึงติดตั้ง กลายเป็นแอพฯจะไม่ได้รับ Permission ใดๆเลยตอนติดตั้ง แต่ต้องไปกด Allow Permission ตอนกำลังจะเข้าถึงฟังก์ชั่นนั้นๆแทน โดยตอนติดตั้งแอพฯจาก Play Store จะไม่มี Dialog สำหรับขอ Permission ขึ้นมาให้เลือกอีกต่อไป

และ Dialog ในการขอ Permission เหล่านี้ไม่ได้เด้งขึ้นมาโดยอัตโนมัติ แต่นักพัฒนาจะต้องสั่งเอง ซึ่งถ้าไม่ทำแล้วเผลอเรียกคำสั่งที่ต้องใช้ Permission แต่ยังไม่ได้รับการอนุญาตจากผู้ใช้แล้วหละก็ ... คำสั่งนั้นๆจะ Throw Exception ออกมา ผลก็คือถ้าไม่ดักไว้ แอพฯจะ Crash ทันที

นอกจากนั้น ถึงแม้ผู้ใช้จะเคยให้สิทธิ์ในการเข้าถึง Permission แล้ว แต่ผู้ใช้ก็สามารถยกเลิกสิทธิ์ (Revoke) ได้ทุกเมื่อผ่านหน้า Settings ของเครื่อง

มาถึงตรงนี้ นักพัฒนาทั้งหลายคงจะขนลุกเกรียวกันแล้ว เพราะจะสัมผัสได้เลยว่า ลอจิคการพัฒนาแอพฯเปลี่ยนไปหมดเลย! จากเดิมที่สั่งอะไรได้หมดเลยไม่เคยมีปัญหา กลายเป็นว่าต้องเขียนโค้ดไปดูไปว่าคำสั่งไหนใช้ Permission อะไร และหากจะเรียกใช้ก็ต้องเช็คเรื่อง Permission ด้วยทุกครั้ง มิฉะนั้นแอพฯ Crash ทันที

ถูกต้องครับ พูดแบบไม่สปอยล์เลยว่ามันเปลี่ยนไปจริงๆ และเปลี่ยนเยอะด้วย การเขียนโค้ดจะต้องซีเรียสขึ้นมากๆ มิฉะนั้นอาจจะเกิดปัญหาได้ทั้งระยะสั้นและระยะยาว

ทั้งนี้ ระบบ Runtime Permission ใหม่นี้จะสามารถใช้งานได้อย่างสมบูรณ์ก็ต่อเมื่อเราเซต targetSdkVersion เป็น 23 ก็คือการระบุว่าเราทดสอบแอพฯบน API Level 23 (Android 6.0 Marshmallow) แล้วนั่นเอง และจะใช้งานได้บน Android Marshmallow เท่านั้น หากเอาแอพฯไปรันในรุ่นต่ำกว่านั้นก็จะทำงานเหมือนเดิมทุกประการ

เกิดอะไรขึ้นกับแอพฯที่ปล่อยไปแล้ว?

อาจจะเกิดอาการ Panic กันเล็กน้อย แบบนี้แล้วแอพฯที่ปล่อยไปแล้วเมื่อ 3 ปีที่แล้ว พอเอาไปลงบน Android Marshmallow แล้วจะโดนด้วยมั้ยหละนี่?

คำตอบคือทางแอนดรอยด์เค้าคิดเผื่อไว้เรียบร้อยแล้ว ก็เลยแบ่งไปว่า ถ้ากำหนด targetSdkVersion ไว้ต่ำกว่า 23 จะถือว่ายังไม่ได้ทดสอบแอพฯให้เข้ากับฟีเจอร์ Runtime Permission และจะสลับไปใช้ในโหมดเก่าแทน ก็คือตอนติดตั้งแอพฯจะขึ้น Dialog ยอมรับ Permission และได้รับ Permission ทุกตัวตอนติดตั้งครับ

คราวนี้โค้ดก็จะรันได้เหมือนเดิม แต่สิ่งที่ต่างกันออกไปจากเดิมคือ ผู้ใช้ยังสามารถสั่งปิด Permission ในภายหลังเป็นตัวๆได้อยู่จากหน้า Settings ของเครื่อง แต่จะมี Dialog ขึ้นมาเตือนว่าแอพฯนี้ถูกออกแบบมาเพื่อรุ่นเก่านะ ถ้าปิด Permission ไปแอพฯอาจจะมีปัญหาได้นะ แต่ถ้ายอมรับได้ก็จะปิด Permission นั้นๆทิ้งได้ครับ

คำถามคงผุดขึ้นมาในหัวทันที งี้แอพฯก็ Crash อ่ะดิ?

ถือเป็นความกรุณาปราณีของทีมออกแบบแอนดรอยด์ที่ในกรณีนี้แอพฯถูกระบุ targetSdkVersion มาต่ำกว่า 23 คำสั่งนั้นๆจะไม่ Throw Exception ออกมา แต่จะไม่เกิดอะไรขึ้นเลยแทน สำหรับคำสั่งที่มีการคืนค่า ก็จะได้ค่ากลับมาเป็น null ไม่ก็ 0

แน่นอนว่าถึงแอพฯจะไม่ Crash จากการเรียกคำสั่ง แต่การที่โค้ดไม่ได้เขียนมารองรับสถานการณ์แบบนี้ แอพฯก็อาจจะ Crash จากการเรียกคำสั่งอื่นต่อได้ เช่น ไม่สามารถดึงภาพจากกล้องมาได้ แต่พยายามเอาภาพไป Process ต่อ ก็เกิด NullPointerException ได้

ก็ยังดีที่เคสเหล่านี้อาจจะเกิดยากสักหน่อยเพราะในระยะแรกๆเชื่อว่าผู้ใช้ส่วนใหญ่คงไม่ไปปิด Permission เล่นกันหรอก หรือถ้ามีคนไปปิดจริงๆ เค้าก็คงจะต้องยอมรับได้ต่อผลที่ตามมา

แต่พอระยะยาวๆก็ไม่แน่ หลังจากผู้ใช้เริ่มเรียนรู้ระบบเหล่านี้ ก็อาจจะเริ่มไปปิด Permission บางตัวกันอย่างแพร่หลาย แล้วการปล่อยให้แอพฯเราทำงานไม่ได้ก็ถือเป็นเรื่องที่ยอมรับไม่ได้

เพื่อให้แอพฯเราทำงานได้อย่างสมบูรณ์แบบ ตอนนี้เราควรจะรีบปรับโค้ดเพื่อให้รองรับระบบ Runtime Permission ให้เรียบร้อยตั้งแต่ตอนนี้ก่อนที่จะสายเกินไป

สำหรับคนที่ยังปรับโค้ดไม่เสร็จ ก็อย่าเพิ่งปรับ targetSdkVersion เป็น 23 แล้วปล่อยเวอร์ชั่นใหม่เป็นอันขาดเชียว เพราะโทษของการปรับค่า targetSdkVersion โดยที่โค้ดยังไม่รองรับคือ "แอพฯ Crash" นะครับ แก้โค้ดให้เสร็จสมบูรณ์ก่อนถึงจะขยับขึ้นนะ

คำเตือน ตอนนี้ Android Studio เวลา New Project ขึ้นมา มันจะตั้ง targetSdkVersion เป็นตัวล่าสุดโดยอัตโนมัติ ซึ่งก็คือ 23 หากยังไม่ได้ปรับโค้ดให้รองรับ Runtime Permission ขอให้ลดเหลือ 22 ก่อน

Permission ที่ได้รับโดยไม่ต้องขอ

ถึงแม้บน Android 6.0 เราจะต้องขอ Permission จากผู้ใช้ก่อน แต่มี Permission อยู่กลุ่มหนึ่งที่ได้รับการเข้าถึงตั้งแต่ติดตั้งเช่นกัน เราเรียกกลุ่มนี้ว่ากลุ่ม Normal Permission (PROTECTION_NORMAL) ได้แก่ Permission ดังต่อไปนี้

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

เพียงแค่กำหนด Permission เหล่านี้ไว้ใน AndroidManifest.xml ก็จะสามารถใช้งานได้ตั้งแต่ตอนติดตั้งครับ นอกจากนั้นผู้ใช้ยังไม่สามารถสั่งปิด Permission เหล่านี้ได้อีกด้วย จึงไม่จำเป็นต้องเช็คการเข้าถึง Permission สำหรับ Permission เหล่านี้

ปรับโค้ดให้รองรับระบบ Permission แบบใหม่

คราวนี้มาทำโค้ดให้รองรับระบบ Runtime Permission อย่างสมบูรณ์แบบกันครับ เริ่มต้นด้วยวิธีตรงกันก่อนละกันนะ ก่อนอื่นเลยให้เซต compileSdkVersion และ targetSdkVersion เป็น 23 ให้เรียบร้อยก่อนครับ

android {
    compileSdkVersion 23
    ...

    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }

โดยตัวอย่างนี้จะสมมติว่าเราจะสร้าง Contact เพิ่มขึ้นมา 1 รายชื่อ ตามคำสั่งด้านล่างนี้

    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());
        }
    }

ซึ่งโค้ดด้านบนจะต้องใช้ Permission WRITE_CONTACTS ในการทำงาน หากฝืนเรียกคำสั่งด้านบนไป แอพฯจะแครชโดยทันที

อย่างแรกที่ต้องทำคือเรื่องเดิมๆ กำหนด Permission ใน AndroidManifest.xml ให้เรียบร้อย

<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

สิ่งที่ต้องทำขั้นต่อไปคือเราจะต้องเขียนโค้ดเพิ่มอีกหนึ่งฟังก์ชั่นเพื่อเช็คก่อนว่าแอพฯเรามี Permission ที่ต้องการหรือยัง หากยังไม่มีให้ขึ้น Dialog ขอ Permission แต่ถ้ามีแล้วก็ให้ทำงานต่อได้เลย

ทั้งนี้ระบบการขอ Permission จะแบ่ง Permission ออกเป็น "กลุ่ม (Permission Group)" ดังนี้

เมื่อผู้ใช้อนุญาต Permission ใด Permission หนึ่งใน Permission Group ใดๆแล้ว Permission อื่นใน Group จะได้รับการอนุญาตร่วมด้วยทันที ยกตัวอย่างเช่นในกรณีนี้ WRITE_CONTACTS อยู่ในกลุ่ม CONTACTS ถ้าเกิดผู้ใช้กดอนุญาตแล้ว แอพฯจะก็ได้สิทธิ์ READ_CONTACTS และ GET_ACCOUNTS ไปด้วยเลยทันที

โค้ดในการตรวจเช็คและขอ WRITE_CONTACTS Permission จะใช้คำสั่ง checkSelfPermission และ requestPermissions ของ Activity ที่เพิ่มมาตอน API Level 23 ดังนี้

    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();
    }

หากมี Permission แล้ว คำสั่ง insertDummyContact() จะถูกเรียกทันที แต่ถ้ายังไม่มี Permission นั้น คำสั่ง requestPermissions จะเด้ง Dialog ขึ้นมาให้ผู้ใช้กดยอมรับดังนี้

และหลังจากผู้ใช้กด Allow หรือ Deny คำสั่ง onRequestPermissionsResult จะถูกเรียกเพื่อแจ้งผลการอนุญาตของผู้ใช้ สามารถเช็คจากตัวแปร grantResults ได้ดังนี้

    @Override
    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);
        }
    }

เพียงเท่านี้โค้ดก็จะรองรับระบบ Runtime Permission แล้ว ซึ่งโค้ดก็วุ่นวายขึ้นพอสมควรเลย ต้องทำแบบนี้ให้ครบทุกกรณีห้ามพลาดเลยแม้แต่กรณีเดียว มิฉะนั้นแอพฯจะมีปัญหาได้ครับ

ทำโค้ดให้รองรับ Never Ask Again ด้วย

สังเกตดูว่าใน Dialog ที่ขอ Permission เราสามารถเลือก Never ask again ได้ด้วย

และหากเลือกออปชั่นตรงนี้แล้วกด Deny เวลาเราเรียก requestPermissions ครั้งต่อๆไป เจ้า Dialog นี้ก็จะไม่ขึ้นมาให้ผู้ใช้ได้กดอีกเลย

อย่างไรก็ตาม ในแง่ของ UX แล้วถือว่าไม่ดี เพราะกดไปแล้วไม่เกิดอะไรขึ้น เราเลยควรจะ Handle กรณีไว้ด้วย โดยก่อนจะสั่ง requestPermissions เราจะเช็คก่อนว่าเราควรจะบอกผู้ใช้หรือไม่ว่าเราจะขอ Permission ไปทำไมผ่านคำสั่ง 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();
    }

ผลการทำงานของโค้ดด้านบนคือ Dialog สำหรับการแจ้งว่าต้องการ Permission ไปทำไมจะเด้งขึ้นใน 2 กรณี

1) Permission นั้นๆถูกร้องขอเป็นครั้งแรก

2) Permission นั้นๆถูกระบุเป็น Never ask again ไว้

ในกรณีหลัง onRequestPermissionsResult จะถูกเรียกพร้อมส่งค่า PERMISSION_DENIED สำหรับ Permission นั้นๆโดยที่โปรแกรมจะไม่มี Dialog สำหรับกด Allow Permission มาให้ผู้ใช้กด

เรียบร้อย

การขอหลายๆ Permission พร้อมกัน

แน่นอนว่ามีบางฟีเจอร์ที่ต้องใช้ Permission หลายตัวพร้อมกัน หากต้องการขอ Permission หลายตัว เราสามารถทำได้ด้วยคำสั่งเดิม แต่โยน Permission เข้าไปในรูปแบบของ Array ได้ทันที และอย่าลืมเช็คเรื่อง Never ask again ของแต่ละ 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;
    }

ส่วนการรับผลก็จะทำผ่านฟังก์ชั่น onRequestPermissionsResult เช่นเดิม โดยคำสั่งนี้จะถูกเรียกเมื่อทุก Permission ถูกได้รับการอนุญาตหรือปฏิเสธหมดแล้ว ท่าที่ใช้รับจึงต้องเปลี่ยนแปลงเล็กน้อยโดยการเอา HashMap เข้ามาช่วย และเช็ค Permission ตามเงื่อนไขที่กำหนดได้ตามต้องการ

    @Override
    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);
        }
    }

ตรงนี้อาจจะต้องลิสต์ออกมาดูเป็นข้อๆดูว่าจะตั้งเงื่อนไขไว้อย่างไร เพราะบางทีหากอนุญาตบาง Permission แต่ปฏิเสธบาง Permission ก็ยังควรจะทำงานได้ หรืออาจจะเลือกหนทางว่าถ้ามี Permission ใดก็ตามถูกปฏิเสธจะไม่ทำงานอะไรเลยก็ทำได้เช่นกัน อันนี้แล้วแต่ออกแบบเลยครับ

ใช้ Support Library เพื่อให้โค้ดสนับสนุนรุ่นเก่า

ถึงโค้ดด้านบนจะใช้งานได้แล้ว แต่ปัญหาคือคำสั่งที่เราเรียก ณ ตอนนี้เป็นของ Activity บน API Level 23 จึงไม่ต้องแปลกใจที่ถ้าเอาแอพฯไปรันบน Android รุ่นต่ำกว่านั้น แอพฯจะ Crash ที่คำสั่งที่เกี่ยวข้องกับ Permission ทันที

ถึงแม้เราจะสามารถใช้วิธีการเช็ค Build Version แบบรันไทม์ได้ด้วยตามโค้ดด้านล่างนี้

        if (Build.VERSION.SDK_INT >= 23) {
            // Marshmallow+
        } else {
            // Pre-Marshmallow
        }

แต่ลอจิคการเขียนโค้ดก็จะวุ่นวายขึ้นไปอีก เราเลยแนะนำให้ใช้อีกวิธีนึงโดยใช้ความช่วยเหลือจาก Support Library v4 ที่รักนั่นเอง โดยแทนที่คำสั่งที่เกี่ยวข้องทั้งสามด้วยคำสั่งดังต่อไปนี้

ContextCompat.checkSelfPermission()

คืนค่าให้อย่างถูกต้องว่าแอพฯได้รับการเข้าถึง Permission นั้นๆหรือไม่ โดยคืนค่า PERMISSION_GRANTED ถ้าแอพฯเข้าถึงได้และคืนค่า PERMISSION_DENIED ถ้าเข้าถึงไม่ได้

ActivityCompat.requestPermissions()

ถ้ารันในรุ่นก่อน M คำสั่ง OnRequestPermissionsResultCallback จะถูกเรียกทันทีพร้อมกับคืนค่า PERMISSION_GRANTED หรือ PERMISSION_DENIED อย่างถูกต้อง

- ActivityCompat.shouldShowRequestPermissionRationale() 

บน Android รุ่นก่อน M คำสั่งนี้จะ return false ทุกกรณี

ให้แทนที่คำสั่ง checkSelfPermission, requestPermissions และ shouldShowRequestPermissionRationale ด้วยคำสั่งเหล่านี้จาก Support Library v4 เสมอ แล้วโค้ดจะรันได้บนทุกรุ่นด้วยลอจิคเดิมครับ โดยคำสั่งพวกนี้จะขอ Context หรือ Activity เพิ่มใน Parameter ก็โยนไปให้ถูกตัวเป็นอันเสร็จเรียบร้อย โค้ดจะหน้าตาเป็นแบบนี้

    private void insertDummyContactWrapper() {
        int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.WRITE_CONTACTS);
        if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
            if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                    Manifest.permission.WRITE_CONTACTS)) {
                showMessageOKCancel("You need to allow access to Contacts",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                ActivityCompat.requestPermissions(MainActivity.this,
                                        new String[] {Manifest.permission.WRITE_CONTACTS},
                                        REQUEST_CODE_ASK_PERMISSIONS);
                            }
                        });
                return;
            }
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[] {Manifest.permission.WRITE_CONTACTS},
                    REQUEST_CODE_ASK_PERMISSIONS);
            return;
        }
        insertDummyContact();
    }

นอกจากนั้น คำสั่งต่างๆเหล่านี้ยังสามารถเรียกใช้จาก Fragment ใน Android Support Library v4 ได้อีกด้วย สามารถใช้งานได้ทันทีครับ

ใช้ 3rd Party Library เข้าช่วยทำให้โค้ดสั้นลง

จะเห็นว่าโค้ดวุ่นวายมาก จึงไม่ต้องแปลกใจที่จะมีนักพัฒนาปล่อย Library เพื่อทำให้โค้ดสั้นลงมาหลายตัว แต่ตัวที่เห็นแล้วดูจะเข้าท่าที่สุดน่าจะเป็นตัว PermissionsDispatcher ของ hotchemi ซึ่งหลักการทำงานไม่มีอะไร มันคือโค้ดที่เราทำกันด้านบนนี่แหละ แค่เขียนสั้นลงแล้วมันจะ Generate โค้ดส่วนที่เหลือให้ แต่แน่นอนว่าความยืดหยุ่น (Flexibility) ในการทำโน่นนี่ก็ลดลงไปด้วย ยังไงลองไปเล่นกันดูได้ครับ

เกิดอะไรขึ้นเมื่อแอพฯถูกเปลี่ยนสถานะ Permission ระหว่างทำงาน?

อย่างที่บอกว่าผู้ใช้สามารถ Revoke Permission ใดๆได้ทุกเมื่อผ่านหน้า Settings ของมือถือ

แล้วจะเกิดอะไรขึ้นถ้าดันไปกด Revoke Permission ตอนที่แอพฯเปิดอยู่? เราลองมาให้แล้ว ผลปรากฎว่า Process ของแอพฯนั้นๆจะถูกทำลาย (Terminate) ลงทันที ทำให้ทุกอย่างที่อยู่ใน Application นั้นๆไม่ว่าจะเป็น Activity, Thread ฯลฯ ถูกทำลายและหยุดทำงานทั้งหมด หากอยากจะใช้งานต้องกลับไปเปิดแอพฯใหม่เท่านั้น ซึ่งเอาจริงๆก็ Make Sense ดี เพราะหากปล่อยให้แอพฯทำงานต่อ คงจะพังพินาศเลยทีเดียว

คำแนะนำ

ถึงตอนนี้น่าจะพอเห็นภาพว่าแล้ว Runtime Permission เป็นเรื่องใหญ่แค่ไหน เพราะมันกระทบต่อการเขียนโค้ดแอนดรอยด์อย่างรุนแรงเลย

อย่างไรก็ตาม ระบบ Permission ใหม่นี้ถูกนำมาใช้จริงแล้วบน Android Marshmallow และหากไม่ปรับโค้ดให้รับรองฟังก์ชั่นตรงนี้ตามหละก็ อาจจะเกิดปัญหา

ข่าวดีคือ Permission ที่ถูกใช้บ่อยๆส่วนใหญ่อยู่ในกลุ่ม Normal Permission เช่น INTERNET ซึ่งไม่จำเป็นต้องจัดการกับ Runtime Permission เลย ส่วน Permission ที่ต้องทำตามบทความนี้จะมีอยู่เพียงไม่กี่ตัวเท่านั้น งานส่วนใหญ่ที่ต้องทำน่าจะเป็นเรื่องของการวางแผนและทำ UX

คำแนะนำตอนนี้มีอยู่สองเรื่องคือ

1) ปรับโค้ดให้รองรับ Runtime Permission โดยด่วน

2) หากโค้ดยังไม่รองรับ Runtime Permission ก็ห้ามกำหนด targetSdkVersion เป็น 23 เป็นอันขาด โดยเฉพาะอย่างยิ่งตอน New Project ต้องดู targetSdkVersion ด้วยว่าตอนนี้ถูกตั้งไว้ที่เท่าไหร่

สำหรับการปรับโค้ดให้รองรับก็ถือว่าเรื่องใหญ่อยู่ เพราะหากออกแบบโครงสร้างโค้ดมาไม่ดีอาจจะต้องถึงขั้นรื้อโค้ดใหม่หมดกันเลย อย่างไรก็ตาม ... ก็คงต้องทำครับ

ในขณะเดียวกัน เนื่องจากแนวคิดการทำแอพฯแอนดรอยด์เปลี่ยนไปจากหน้ามือเป็นหลังมือ กลายเป็นว่าหากไม่ได้รับบาง Permission แอพฯก็ยังต้องทำงานต่อได้ แต่จะทำในลักษณะไหนแค่นั้นเอง เราเลยแนะนำให้ลิสต์ฟีเจอร์ทั้งหมดที่เกี่ยวข้องกับ Permission ออกมาเป็นข้อๆอย่างชัดเจน แล้วเขียนออกมาเลยว่า Permission ไหนจำเป็นชนิดที่ถ้าไม่มีจะทำงานไม่ได้ และ Permission ไหนเป็นแค่ Optional รวมถึงลิสต์ออกมาให้ครบทุกเคสว่าถ้ามี Permission นี้แต่ไม่มี Permission นี้จะให้การทำงานออกมาเป็นอย่างไร จากนั้นถึงเริ่มลุยโค้ดกันครับ

ก็ขอให้ทุกท่านมีความสุขกับการปรับโค้ดครั้งนี้ เริ่มปรับตั้งแต่วันนี้เลยครับ ในวันที่ Android M ออกแอพฯจะได้ไม่มีปัญหาใดๆ

Happy Coding ครับ

ผู้เขียน: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory)
นักพัฒนาแบบ Full-Stack ที่มีประสบการณ์ในการพัฒนาแอพฯแอนดรอยด์มากว่า 6 ปีและอยู่ในวงการพัฒนาแอพฯมือถือมากว่า 12 ปี มีความสนใจทางด้าน Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking และรักที่จะแชร์เรื่องราวให้ผู้คนได้อ่านได้ฟังกันผ่าน Blog