Android

Yêu cầu Permission trong Runtime trên Android Marshmallow về sau

Mặc dù đã ra đời từ rất lâu rồi, đúng hơn là cùng lúc với Android M Preview (tức Marshmallow sau này, hay Android 6.0 API23) từ năm 2015, tuy nhiên có không ít các nơi dạy Android không nhắc, cũng như tâm lí các bạn tự “mò mẫm” viết code Android không (thèm) để ý, tới Runtime permissions và đây là một thiếu sót vô cùng trầm trọng. Bởi lẽ, nếu hạn chế, tránh né Runtime permissions, thì bạn đang làm khó chính bạn, khi tự đặt giới hạn hoạt động cho ứng dụng của mình. Đó là “nguyên nhân khách quan sự ra đời” của bài viết này.

1. Các permissions trong Android, và các dangerous permissions trong số đó:

Nói một cách ngắn gọn súc tích nhất, permissions là những khả năng mà Android cung cấp, cho phép và kiểm soát ứng dụng của bạn. Chẳng hạn, Android cung cấp khả năng truy cập internet cho các ứng dụng do nó quản lí, và ứng dụng của bạn muốn truy cập internet, thì phải “xin xỏ” nó. Tình huống giống như bạn xin Giám đốc của bạn: “Sếp ơi, cho phép em nghỉ ngày mốt vì em bận đưa ông em đi tập gym và bà em đi bơi nhé”. Sếp cho phép thì bạn mới được nghỉ làm, và tương tự, Android cấp phép truy cập internet thì ứng dụng của bạn mới được “nhìn thấy nội dung online”, chẳng hạn gửi và nhận HTTP Request. Bằng không thì ứng dụng chỉ còn có thể “chơi với dế” mà thôi.

Thực tế, thì cho tới đời Android 5.1 Lollipop được ấn định mức API là 22, thì việc xin xỏ permissions chỉ mang tính “cho có để đó” đối với lập trình viên. Vì thứ nhất, bạn chỉ cần khai báo tất tần tật các permissions trong Manifest là được. Và thứ hai là Android system sẽ “nhắm mắt đưa chân”, cấp phép tất cả các permissions đó khi người dùng chấp nhận cài đặt ứng dụng mà chả thèm đếm xỉa tới tâm lí của chủ thiết bị: “Oke, ông/bà chấp nhận thì chấp nhận cả rổ, còn không thì khỏi cài luôn. Thích cái nào đây?”. Điều này dẫn tới những trường hợp mã độc tung hoành khắp thiết bị, đặc biệt là trên những thiết bị mà chủ của chúng thích root để gian lận trong các games, hoặc hay cài đặt ứng dụng từ những nguồn chưa xác định dù là ứng dụng đó đang miễn phí trên Google Play nhưng vẫn thích cài từ bên ngoài, hoặc tệ hơn nữa là những ứng dụng tràn lan trên Google Play thích truy cập vào những thông tin mà chức năng chính của chúng chả cần thiết phải đọc qua, chẳng hạn như ứng dụng đèn pin nhưng truy cập vào đủ thứ thông tin như vị trí của bạn, danh bạ của bạn, cuộc gọi của bạn, v.v…

Thực tế đối thủ truyền kiếp của Android là iOS đã thực hiện cấp permissions trong runtime từ những ngày đầu tiên, và tới mấy năm ròng rã sau đó thì Google mới đưa vào chú robot xanh. Trễ vẫn còn hơn không. Từ Android 6.0 Marshmallow API23, Android system không còn “ăn cơm hớt” mà cấp các permissions một cách tự ý nữa. Đúng hơn là các dangerous permissions. Những permissions này được nhóm lại và người dùng sẽ quyết định có cấp permissions đó hay không, và cũng chấm dứt luôn tình trạng “con sâu làm rầu nồi canh”. Bạn thích ứng dụng đó nhưng không muốn cho nó truy cập vị trí của bạn thì cứ từ chối cấp quyền vị trí mà thôi, còn mấy cái quyền khác thì vẫn chạy bình thường, tức là “chỉ đơn giản lấy con sâu ra khỏi nồi canh và ăn tiếp như thường”.

Các dangerous permissions là do người dùng cấp, và hệ thống Android sẽ “hỏi giùm” cho ứng dụng của bạn với cái dialog như bên dưới. Về phần các dangerous permissions gồm những gì thì tôi sẽ giải thích sau. Các permissions không thuộc nhóm dangerous permissions sẽ được nhóm vào “phe” normal permissions và những normal permissions này sẽ được cấp quyền theo kiểu cũ, tức là Android system sẽ tự cấp những quyền đó cho ứng dụng khi chúng chạy và bạn không cần và cũng không thể request mấy pờ-mít-sần này theo kiểu dangerous permissions để Android system hiện cái bảng bên dưới ra.android-runtime-permission-request

Danh sách các dangerous permissions nằm ở phía dưới. Trong đó, bản chất của những permissions nằm bên trong chính là những constants trong android.Manifest.permission class (là những public static final String).

  • CALENDAR
    • READ_CALENDAR
    • WRITE_CALENDAR
  • CAMERA
    • CAMERA
  • CONTACTS
    • READ_CONTACTS
    • WRITE_CONTACTS
    • GET_ACCOUNTS
  • LOCATION
    • ACCESS_FINE_LOCATION
    • ACCESS_COARSE_LOCATION
  • MICROPHONE
    • RECORD_AUDIO
  • PHONE
    • READ_PHONE_STATE
    • READ_PHONE_NUMBERS
    • CALL_PHONE
    • ANSWER_PHONE_CALLS
    • READ_CALL_LOG
    • WRITE_CALL_LOG
    • ADD_VOICEMAIL
    • USE_SIP
    • PROCESS_OUTGOING_CALLS
  • SENSORS
    • BODY_SENSORS
  • SMS
    • SEND_SMS
    • RECEIVE_SMS
    • READ_SMS
    • RECEIVE_WAP_PUSH
    • RECEIVE_MMS
  • STORAGE
    • READ_EXTERNAL_STORAGE
    • WRITE_EXTERNAL_STORAGE

2. Request runtime permissions với thư viện Support:

Tôi sẽ không hướng dẫn bạn cách khai báo permissions trong Manifest ở đây, vì điều đó là quá cơ bản, và nếu bạn không biết khai báo pờ-mít-sần trong Manifest (và cũng lười nghiên cứu) thì bạn nên bỏ nghề code, đó là điều kiện cần để được Android system hiện ra cái dialog hỏi cấp quyền như hình bên trên. Còn điều kiện đủ, là bạn phải gọi các methods trong Activity. Thực chất, các methods kiểm tra đã cấp quyền hay chưa cũng như xin quyền khởi thủy nằm trong class Context, hoặc ContextCompat với tham số đầu tiên là Context, nên về lí thuyết, bạn có thể gọi chúng trong Service, hoặc một methods trong class khác lấy tham số từ một Context không null. Tuy nhiên, trên thực tế thì void onRequestPermissionsResult lại là thứ đặc sản của Activity – và qua cái tên là bạn hiểu nó có tác dụng gì rồi phải không – nên bạn bao giờ cũng phải check và request permissions trong Activity cả.

Ở đoạn này, ngược lại với phong cách bình thường, tôi sẽ giới thiệu cách request bằng thư viện Support trước. Và bạn cũng đã đoán được, tôi sẽ hướng dẫn bạn request với class ContextCompat. Phần dùng thư viện Framework, tôi sẽ trình bày ở đoạn sau.

Cũng giống như bạn tới nhà một ai đó để tìm gặp người đó, trước khi “bộc bạch tâm tư” thì bạn phải hỏi những người khác là có người kia ở nhà không. Ở đây cũng vậy, trước khi request một permission hay một loạt permission (dưới dạng array) thì bạn cần và nên kiểm tra coi là ứng dụng đã được cấp quyền đó chưa. Trên thực tế, việc request permission thông thường chỉ diễn ra khi người dùng và ứng dụng của bạn “động chạm” tới permission đó lần đầu, và nếu người dùng đã cấp quyền rồi (permission(s) granted) thì “cứ vậy mà triển khai” luôn, thay vì hỏi lại mỗi lần, sẽ gây cảm giác bực bội cho các “thượng đế”. Để kiểm tra permission(s) đó đã được granted chưa, bạn sử dụng method bên dưới:

Trong đó, Activity activity chính là activity hiện tại (nên bạn truyền luôn this vào đó), còn String permission chính là một trong số những công-xăng về pờ-mít-sần trong android.Manifest.permission. Method này sẽ return một int instance (với Kotlin là Int) là một trong hai giá trị: PackageMananger.PERMISSION_GRANTED (= 0) hoặc PackageManager.PERMISSION_DENIED (= -1). Do đó, bạn có thể đặt một boolean cho dễ kiểm tra nếu bạn phải sử dụng method này nhiều lần. Chẳng hạn:

Trong trường hợp giá trị int bên trên trả về là 0, hoặc boolean trên có giá trị là true, nghĩa là permissions đã được cấp rồi và bạn tiến hành thao tác bình thường. Còn nếu int trả về -1 hoặc boolean bị false, có nghĩa là pờ-mít-sần chưa được cấp phép, và cũng có nghĩa là bạn phải cho ứng dụng của mình “xin xỏ” người dùng với các method sau:

Argument cuối cùng, là int requestCode nghe quen quen đúng không? Nó giống giống startActivityForResult(Intent intent, int requestCode) đúng không? Và sự thật chính là nó đó. Nội dung của ActivityCompat.requestPermissions(…) có gọi cái method startActivityForResult với cái requestCode được đem “bê nguyên si” từ ngoài vào trong. Và bạn đang nghĩ tới onActivityResult(…)? Đúng đó, và nếu như bạn đã quen với đao to búa lớn thì bạn có thể xử lí trong đó luôn. Nhưng sẽ tốt hơn nếu bạn xử lí trong onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults), vì “làm ăn” trong đây dễ hơn, cũng như tránh bớt quá nhiều code trong onActivityResult, cũng như bạn có gọi nhiều startActivityForResult với nhiều mục đích khác.

Trong onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults), thì phần argument thứ hai là danh sách các permissions đã được gửi đi, và arguments thứ ba chính là các kết quả cấp phép tương ứng với thứ tự các permissions trong argument thứ hai. Và đương nhiên, length của chúng phải bằng nhau. Ví dụ, grantResult của permission có index là n trong permissions sẽ có index cũng là n trong grantResults. Và đúng như bạn đang “gia cát dự”, giá trị của mỗi grantResult hoặc là PackageManager.PERMISSION_GRANTED, hoặc là PackageManager.PERMISSION_DENIED.

Ngoài ra, bạn nên đưa ra lời giải thích là ứng dụng của bạn cần được cấp những quyền gì và vì sao người dùng cần cấp những quyền đó trước khi “đập vào mặt” người dùng một mớ thông báo qua method requestPermissions. Lời giải thích cần phải rõ ràng và chính xác, để họ hiểu và cấp quyền ngay để ứng dụng hoạt động được đúng như bạn mong đợi. Tổng hợp lại, tôi đưa ra phần code mẫu như sau, với permission ví dụ là RECORD_AUDIO để ghi âm:

3. Request runtime permissions với thư viện Framework:

Thay vì sử dụng thư viện Support, bạn có thể dễ dàng sử dụng các methods có sẵn trong thư viện Framework nằm sẵn trong hệ thống, với các methods tương ứng là Context#checkSelfPermission(String permission)Activity#requestPermissions(String[] permissions, int requestCode) và xử lí kết quả của permission requests cũng trong onRequestPermissionsResult. Tuy nhiên, ứng dụng có minSdk thấp hơn 23, thì bạn lúc nào cũng phải bao bọc phần code trong cặp ngoặc nhọn của if (Build.SDK.VERSION >= 23). Tất nhiên, gọi nhiều quá có thể làm bạn nổi điên, vì vậy, bạn nên cân nhắc dùng thư viện Support vì trong phần code thì chúng đã if sẵn cho bạn rồi.

4. Lưu ý về Runtime permissions:

Nếu bạn đã request một permission trong một permission group rồi, thì khi ứng dụng cần, Android sẽ cấp các quyền khác trong nhóm đó luôn cho ứng dụng. Chẳng hạn, nếu người dùng đã cấp READ_SMS cho ứng dụng rồi, thì khi ứng dụng cần quyền WRITE_SMS, mặc dù có thể bạn sẽ vẫn sẽ request, nhưng Android system sẽ tự cấp quyền cho bạn mà không thông qua người dùng.

Cuối cùng, chúc các bạn có thể dễ dàng requestPermissions để người dùng cấp phép và ứng dụng của bạn chạy như mong đợi.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.