1. 程式人生 > >Android應用申請執行時許可權(Permission)

Android應用申請執行時許可權(Permission)

轉載地址:http://blog.csdn.net/gaugamela/article/details/56277793

一直以來,為了保證最大的安全性,安裝Android應用時,系統總是讓使用者選擇是否同意該應用所需的所有許可權。

一旦安裝應用,就意味著該應用所需的所有許可權均已獲得。 
若在使用某個功能時用到了某個許可權,系統將不會提醒使用者該許可權正在被獲取(比如微信需要使用攝像頭拍照, 
在Android 6.0以前的裝置上,使用者將不會被系統告知正在使用“使用系統攝像頭”的許可權)。

這在安全性上是個隱患:在不經使用者同意的情況下,一些應用在後臺可以自由地收集使用者隱私資訊而不被使用者察覺。

為了解決這個問題,從Android 6.0版本開始,在安裝應用時,該應用無法取得任何許可權。 
相反,在使用應用的過程中,若某個功能需要獲取某個許可權,系統會彈出一個對話方塊,顯式地由使用者決定是否將該許可權賦予應用。 
只有得到了使用者的許可,該功能才可以被使用。

需要注意的是,賦予許可權的對話方塊並不會自動彈出,而需要由開發者手動呼叫。 
若程式呼叫的某個方法需要使用者賦予相應許可權,而此時該許可權並未被賦予時,那麼程式就會丟擲異常並崩潰。 
除此之外,使用者還可以在任何時候,通過設定中的應用管理撤銷賦予過的許可權。

應用的targetSDKVersion < 23時,許可權檢查仍是早期的形式(僅在安裝時賦予許可權,使用時將不被提醒); 
應用的targetSDKVersion ≥ 23時,則將使用新的執行時許可權規則。

以下羅列了在安裝應用時,自動賦予應用的許可權,這些許可權無法在安裝後手動撤銷,我們稱其為基本許可權(Normal Permission)。 
開發者僅需要在AndroidManifest.xml中宣告這些許可權,應用就能自動獲取無需使用者授權。

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 Group),如下表所示:

若應用被賦予了某個許可權組中的一個許可權(比如READ_CONTACTS許可權被賦予), 
那麼該組中的其他許可權將被自動獲取(WRITE_CONTACTS和GET_ACCOUNTS許可權被自動獲取)。

檢查和申請許可權的方法分別是Activity.checkSelfPermission()和Activity.requestPermissions,這兩個方法是在 API 23 中新增的。

程式碼示例如下:

............
mDialButton = (Button) v.findViewById(R.id.crime_dial);
mDialButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= 23) {
            //我是在Fragment裡寫程式碼的,因此呼叫getActivity
            //如果不想判斷SDK,可以使用ActivityCompat的介面來檢查和申請許可權
            int hasReadContactsPermission = getActivity().checkSelfPermission(
                    android.Manifest.permission.READ_CONTACTS);

            if (hasReadContactsPermission != PackageManager.PERMISSION_GRANTED) {
                //這裡就會彈出對話方塊
                getActivity().requestPermissions(
                        new String[] {Manifest.permission.READ_CONTACTS},
                        ASK_READ_CONTACTS_PERMISSION);

                return;
            }

            //高版本中檢查是否有執行時許可權,具有許可權時才呼叫
            getPhoneNumberAndDial();
        } else {
            //在AndroidManifest.xml中仍然宣告使用"android.permission.READ_CONTACTS"
            //在低版本中直接呼叫該函式
            getPhoneNumberAndDial();
        }
    }
});
............

如前文所述,在Android高版本中,應用初始安裝時,即使聲明瞭執行時許可權,Android系統也不會為其賦予任何許可權。

如下圖所示: 

此時,點選按鍵呼叫Activity的requestPermissions時,將會彈出對話方塊,類似於下圖所示(不同裝置商有不同的定製): 

無論選擇的是“允許”還是“拒絕”,系統都將回調Activity.onRequestPermissionsResult()方法, 
並將選擇的結果傳到方法的第三個引數中。

此時的處理程式碼示例如下:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case ASK_READ_CONTACTS_PERMISSION:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                getPhoneNumberAndDial();
            } else {
                Toast.makeText(getContext(),
                        "READ_CONTACTS Denied",
                        Toast.LENGTH_SHORT)
                        .show();
            }
            return;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

需要注意的是: 
目前,如果在原生的Fragment中呼叫Activity.requestPermissions函式時, 
onRequestPermissionsResult必須實現在包含該Fragment的Activity中。

如果是在android.support.v4.app.Fragment中實現相同功能時, 
就可以直接在Fragment中定義onRequestPermissionsResult函式, 
並可以藉助於相容庫,此時程式碼如下:

................
mDialButton = (Button) v.findViewById(R.id.crime_dial);
mDialButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //checkSelfPermission、requestPermissions均是呼叫相容庫中的函式,此時不再需要判斷SDK
        int hasReadContactsPermission = checkSelfPermission(getContext(),
                android.Manifest.permission.READ_CONTACTS);

        if (hasReadContactsPermission != PackageManager.PERMISSION_GRANTED) {
             requestPermissions(
                     new String[] {Manifest.permission.READ_CONTACTS},
                     ASK_READ_CONTACTS_PERMISSION);
             return;
         }

         getPhoneNumberAndDial();
    }
});
..................

如上圖所示,每當系統申請許可權時,彈出的對話方塊會有一個類似於“拒絕後不再詢問”的勾選項。 
若使用者打了勾,並選擇拒絕,那麼下次程式呼叫Activity.requestPermissions()方法時,將不會彈出對話方塊,許可權也不會被賦予。

這種沒有反饋的互動並不是一個好的使用者體驗。 
所以,下次啟動時,程式應彈出一個對話方塊,提示使用者類似於“您已經拒絕了使用該功能所需要的許可權,若需要使用該功能,請手動開啟許可權”的資訊, 
此時應呼叫Activity.shouldShowRequestPermissionRationale()方法,示例程式碼如下:

............
mDialButton = (Button) v.findViewById(R.id.crime_dial);
mDialButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        int hasReadContactsPermission = checkSelfPermission(getContext(),
                android.Manifest.permission.READ_CONTACTS);

        if (hasReadContactsPermission != PackageManager.PERMISSION_GRANTED) {
            //判斷是否點選過“拒絕並不再提示”,若點選了,則應用自己彈出一個Dialog
            if (!shouldShowRequestPermissionRationale(android.Manifest.permission.READ_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.READ_CONTACTS},
                                    ASK_READ_CONTACTS_PERMISSION);
                                }
                            });
                 return;
             }

             requestPermissions(
                     new String[] {Manifest.permission.READ_CONTACTS},
                     ASK_READ_CONTACTS_PERMISSION);

             return;
         }

         getPhoneNumberAndDial();
    }
});
...............
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(getContext())
            .setMessage(message)
            .setPositiveButton("OK", okListener)
            .setNegativeButton("Cancel", null)
            .create()
            .show

此時,應用第一次申請許可權及使用者勾選了“不再詢問”複選框時,均會彈出類似如下的對話方塊: 

若第一次申請時點選OK,將會彈出許可權申請的介面;

使用者勾選過“拒絕後不再詢問時”,點選OK不會再次拉起申請介面, 
同時onRequestPermissionsResult中收到的結果為PackageManager.PERMISSION_DENIED

最後看看同時申請多個執行時許可權的程式碼示例,其思想基本與前文一致,這段程式碼是直接借鑑過來的:

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 ";
            for (int i = 0; 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) {
    //若配置了該許可權,返回true
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        //若未配置該許可權,將其新增到所需許可權的集合,返回true
        permissionsList.add(permission);

        // 若使用者勾選了“永不詢問”複選框,並拒絕了許可權,則返回false
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }

    return true;
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: {
            //初始化Map集合,其中Key存放所需許可權,Value存放該許可權是否被賦予
            Map<String, Integer> perms = new HashMap<String, Integer>();

            // 向Map集合中加入元素,初始時所有許可權均設定為被賦予(PackageManager.PERMISSION_GRANTED)
            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);

            // 將第二個引數回傳的所需許可權及第三個引數回傳的許可權結果放入Map集合中,由於Map集合要求Key值不能重複,所以實際的許可權結果將覆蓋初始值
            for (int i = 0; i < permissions.length; i++)
                perms.put(permissions[i], grantResults[i]);

                // 若所有許可權均被賦予,則執行方法
                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();
                } 
                //否則彈出toast,告知使用者需手動賦予許可權
                else {
                    // Permission Denied
                    Toast.makeText(MainActivity.this, 
                        "Some Permission is Denied",
                        Toast.LENGTH_SHORT)
                        .show();
                }
            }
            break;

        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

P.S.

根據permission名稱,獲取PermissionInfo的程式碼類似於:

................
    public static List<PermissionInfo> getPermissionInfos(Context context,
            String... permissions) {
        List<PermissionInfo> rst = new ArrayList<>();

        PackageManager pm = context.getPackageManager();

        try {
            for (String name : permissions) {
                rst.add(pm.getPermissionInfo(name, 0));
            }
        } catch (PackageManager.NameNotFoundException e) {
        }

        return rst;
    }
..............

PermissionInfo中主要儲存Permission對應的ProtectionLevel。