「Android6.0許可權適配| 掘金技術徵文 」

Code4Android發表於2019-03-04

原始碼傳送門

前言

現在談論Android許可權適配可能有點沒必要,因為網上關於許可權適配的文章很多,搜一下Android6.0許可權適配關鍵詞能搜到一堆文章,而且很多寫的還很不錯。不過自己想了想還是總結一下,因為那些文章都是別人的,不是自己的,之前一直想總結一下,但是一直沒做,今天就簡單記錄一下,方便以後查閱,也對Android6.0的許可權機制再次進行一次全面的認識。
從Android M開始,使用者開始在應用執行時向其授予許可權,而不是在應用安裝時授予。這樣更友好的讓使用者選擇,當真正需要許可權的時候再去申請許可權,而不是Android M之前在安裝時一下子去申請。

正常許可權

正常許可權不會直接給使用者隱私權帶來風險。如果您的應用在其清單中列出了正常許可權,系統將自動授予該許可權。而不需要我們去請求許可權。

ACCESS_LOCATION_EXTRA_COMMANDS  
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS複製程式碼

危險許可權

危險許可權涵蓋應用需要涉及使用者隱私資訊的資料或資源,或者可能對使用者儲存的資料或其他應用的操作產生影響的區域。例如,能夠讀取使用者的聯絡人屬於危險許可權。如果應用宣告其需要危險許可權,則使用者必須明確嚮應用授予該許可權。

在危險許可權中,我們需要了解一個許可權組的概念,所有危險的 Android 系統許可權都屬於許可權組,如果應用請求其清單中列出的危險許可權,而應用目前在許可權組中沒有擁有任何許可權,則系統會向使用者顯示一個對話方塊,描述應用要訪問的許可權組。對話方塊不描述該組內的具體許可權。

例如我們需要讀取獲取手機卡imsi,此時需要請求許可權READ_PHONE_STATE,發現此時提示框也展示了請求打電話許可權。(系統只告訴使用者應用需要的許可權組,而不告知具體許可權)其實READ_PHONE_STATE和打電話許可權CALL_PHONE都屬於一個許可權組PHONE,如果我們此時允許了許可權,那麼下次再其他地方使用了打電話許可權時系統將立即授予該許可權。

「Android6.0許可權適配| 掘金技術徵文 」
這裡寫圖片描述

注:任何許可權都可屬於一個許可權組,包括正常許可權和應用定義的許可權。但許可權組僅當許可權危險時才影響使用者體驗。可以忽略正常許可權的許可權組。

許可權組 許可權
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
CALL_PHONE
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

許可權請求API

    /**
     * 確定許可權是否已經被授予
     * @param permission 被檢測許可權的名字.
     * @return {@link android.content.pm.PackageManager#PERMISSION_GRANTED} 如果許可權被授予,
     * {@link android.content.pm.PackageManager#PERMISSION_DENIED} 如果許可權被拒絕返回值.
     */
    public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) 



    /**
     * 是否顯示自定義UI提示使用者
     * 華為手機測試 第一次使用時返回false
     * 如果拒絕返回true
     * 如果拒絕並點選不在提醒返回false
     * 已經同意過許可權,但在設定拒絕此時返回true
     * 沒有同意過許可權,在設定中開啟並拒絕許可權返回false
     * @param activity   請求許可權Activity.
     * @param permission 需要請求的許可權.
     * @return 是否顯示自定義對話方塊提示使用者.
     */
    public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity,
                                                               @NonNull String permission)




    /**
     * 給應用申請許可權,申請的許可權必須在manifest檔案註冊,正常許可權在安裝時自動被授權,不需要使用此方法請求許可權
     * 請求之後會彈出系統提示框,供我們選擇是拒絕還是允許,點選後
     * android.support.v4.app.ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult(
     * int, String[], int[])} 方法將會被回撥,
     * @param activity 請求許可權的Activity.
     * @param permissions 需要請求的許可權.
     * @param requestCode 指定一個請求碼,用於區別返回結果
     *
     */
    public static void requestPermissions(final @NonNull Activity activity,
                                          final @NonNull String[] permissions, final int requestCode)



    /**
     * 呼叫requestPermissions方法請求許可權的回撥
     *需要注意的是可能請求的許可權與使用者互動中斷;正在這種情況下回撥將接收一個空的permissions和grantResults陣列
     * @param permissions 請求的許可權. 不為null,長度可能為0.
     * @param grantResults 請求許可權的結果PERMISSION_GRANTED表示許可權被允許,PERMISSION_DENIED表示許可權被拒絕
     */
    void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                    @NonNull int[] grantResults)複製程式碼

需要注意的是,對於如果在Activity中請求許可權則可使用上面API ActivityCompat類,如果在Frament請求許可權則,需要使用Fragment類中的對應方法,否則回撥會有問題。

簡單封裝

 /**
     * 判斷是否具備所有許可權
     *
     * @param permissions 所有許可權
     * @return true 具有所有許可權  false沒有具有所有許可權,此時包含未授予的許可權
     */
    public static boolean isHasPermissions(String... permissions) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return true;
        for (String permission : permissions) {
            if (!isHasPermission(permission))
                return false;
        }
        return true;
    }

    /**
     * 判斷該許可權是否已經被授予
     *
     * @param permission
     * @return true 已經授予該許可權 ,false未授予該許可權
     */
    private static boolean isHasPermission(String permission) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
            return true;
        return ContextCompat.checkSelfPermission(MyApplication.getAppContext(), permission) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * 請求許可權,經測試發現TabActivity管理Activity時,在Activity中請求許可權時需要傳入父Activity物件,即TabActivity物件
     * 並在TabActivity管理Activity中重寫onRequestPermissionsResult並分發到子Activity,否則回撥不執行  。TabActivity回撥中  呼叫getLocalActivityManager().getCurrentActivity().onRequestPermissionsResult(requestCode, permissions, grantResults);分發到子Activity

     * 
     *
     * @param object      Activity or Fragment
     * @param requestCode 請求碼
     * @param permissions 請求許可權
     */
    public static void requestPermissions(Object object, int requestCode, String... permissions) {
        ArrayList<String> arrayList = new ArrayList<>();
        for (String permission : permissions) {
            if (!isHasPermissions(permission)) {
                arrayList.add(permission);
            }
        }
        if (arrayList.size() > 0) {
            if (object instanceof Activity) {
                Activity activity = (Activity) object;
                Activity activity1 = activity.getParent() != null && activity.getParent() instanceof TabActivity ? activity.getParent() : activity;
                ActivityCompat.requestPermissions(activity1, arrayList.toArray(new String[]{}), requestCode);
            } else if (object instanceof Fragment) {
                Fragment fragment = (Fragment) object;
                //當Fragment巢狀Fragment時使用getParentFragment(),然後在父Fragment進行分發,否則回撥不執行
                Fragment fragment1 = fragment.getParentFragment() != null ? fragment.getParentFragment() : fragment;
                fragment1.requestPermissions(arrayList.toArray(new String[]{}), requestCode);
            } else {
                throw new RuntimeException("the object must be Activity or Fragment");
            }
        }
    }複製程式碼

如果想展示自定義UI友好的提示使用者申請該許可權的原因,則需要使用shouldShowRequestPermissionRationale方法,簡要封裝如下

 public static boolean shouldShowRequestPermissionRationale(@NonNull Object object, String... permissions) {
        for (String permission : permissions) {
            if (object instanceof Activity) {
                if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) object, permission)) {
                    return true;
                }
            } else if (object instanceof Fragment) {
                if(((Fragment) object).shouldShowRequestPermissionRationale(permission)){
                    return true;
                }
            } else {
                throw new RuntimeException("the object must be Activity or Fragment");
            }


        }
        return false;
    }

    /**
     * 二次申請許可權時,彈出自定義提示對話方塊
     *
     * @param activity
     * @param message
     * @param iPermissionRequest
     * @see com.example.xh.ui.BaiduLocationFragment 可以檢視該類onRequestPermissionsResult方法當選擇永不提醒時的處理辦法。
     */
    public static void showDialog(Activity activity, String message, final IPermissionRequest iPermissionRequest) {
        new AlertDialog.Builder(activity)
                .setPositiveButton("允許", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        iPermissionRequest.agree();
                        dialog.dismiss();
                    }
                })
                .setNegativeButton("拒絕", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        iPermissionRequest.refuse();
                        dialog.dismiss();
                    }
                })
                .setCancelable(false)
                .setMessage(message)
                .show();
    }複製程式碼

彈出對話方塊後,點選了拒絕或者允許後,給一個回撥,方便進行不同的處理,當然如果統一處理的話,就不需要寫介面,直接在上述點選允許的時候請求許可權,點選不允許的時候,顯示一個Toast再次做下許可權拒絕提示。當然也可在onRequestPermissionsResult中進行判斷,當選中永不提醒後給使用者一個友好跳轉到許可權設定介面。
介面方法

public interface IPermissionRequest {
    void agree();
    void refuse();
}複製程式碼

特殊許可權

有許多許可權其行為方式與正常許可權及危險許可權都不同。SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS 特別敏感,因此大多數應用不應該使用它們。如果某應用需要其中一種許可權,必須在清單中宣告該許可權,並且傳送請求使用者授權的 intent(注意特殊許可權和危險許可權請求方式不一樣)。系統將向使用者顯示詳細管理螢幕,以響應該 intent。

請求WRITE_SETTINGS許可權

  /**
     * 測試請求WRITE_SETTINGS許可權
     */
    @OnClick(R.id.request_write_setting)
    @TargetApi(android.os.Build.VERSION_CODES.M)
    public void requestWriteSetting() {
        if (!Settings.System.canWrite(this)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, requestCodeWriteSetting);
        } else {
            Toast.makeText(PermissionTestActivity.this, "WRITE_SETTINGS 已經被授權", Toast.LENGTH_SHORT).show();
        }
    }


    @TargetApi(Build.VERSION_CODES.M)
    private void showToast() {
        if (Settings.System.canWrite(this)) {
            //檢查返回結果
            Toast.makeText(PermissionTestActivity.this, "WRITE_SETTINGS 被授權", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(PermissionTestActivity.this, "WRITE_SETTINGS 沒有被授權", Toast.LENGTH_SHORT).show();
        }
    }

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == requestCodeWriteSetting) {
            showToast();
        }else if(requestCode==requestCodeAlertWindow){
            showToastAlerterWindow();
        }
    }複製程式碼

請求SYSTEM_ALERT_WINDOW許可權

 /**
     * 測試請求SYSTEM_ALERT_WINDOW許可權
     */
    @OnClick(R.id.request_alert_window)
    @TargetApi(android.os.Build.VERSION_CODES.M)
    public void requestAlertWindow() {
        if (!Settings.canDrawOverlays(this)) {
            Intent intent = new Intent(Settings. ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, requestCodeAlertWindow);
        } else {
            Toast.makeText(PermissionTestActivity.this, "SYSTEM_ALERT_WINDOW 已經被授權", Toast.LENGTH_SHORT).show();
        }
    }
 @TargetApi(Build.VERSION_CODES.M)
    private void showToastAlerterWindow() {
        if (Settings.System.canWrite(this)) {
            //檢查返回結果
            Toast.makeText(PermissionTestActivity.this, "SYSTEM_ALERT_WINDOW 被授權", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(PermissionTestActivity.this, "SYSTEM_ALERT_WINDOW 沒有被授權", Toast.LENGTH_SHORT).show();
        }
    }複製程式碼

「Android6.0許可權適配| 掘金技術徵文 」
WRITE_SETTINGS許可權設定介面

「Android6.0許可權適配| 掘金技術徵文 」
SYSTEM_ALERT_WINDOW許可權設定介面

注意:許可權必須在清單檔案中宣告,否則進入上面介面時開關是不可點選的灰色。

開啟許可權設定介面

在上面危險許可權申請中,如果使用者拒絕了許可權,並且選中永不提醒,那麼下次請求許可權時直接執行onRequestPermissionsResult回撥,並且返回狀態是許可權被拒絕狀態,那麼若想授予許可權,必須去手機的許可權管理中設定,如果使用者去手機裡找是不是很麻煩,況且一步人不知道設定許可權的地方在哪,那麼為了程式的體驗更好,我們可以在我們的應用中引導使用者跳轉到設定許可權的介面。實現程式碼如下

    /**
     * 開啟應用許可權設定介面
     */
    @OnClick(R.id.open_permission_setting)
    public void requestOpenPermissionSetting() {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        // Uri uri = Uri.fromParts("package", getPackageName(), null);
        Uri uri1=Uri.parse("package:" + getPackageName());
        intent.setData(uri1);
        startActivity(intent);
    }複製程式碼

介紹到此就結束了,水平有限若有問題請指出,Hava a wonderful day.

最後放幾篇感覺不錯的文章:

Android6.0許可權適配之WRITE_EXTERNAL_STORAGE(SD卡寫入)

谷歌文件 在執行時請求許可權

谷歌文件 系統許可權

Android許可權機制與適配經驗

本次徵文活動的連結: juejin.im/post/58d8e9…

相關文章